summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/visualize/pattern.rs
blob: 2017ea65520fc227081361a571493037b7f7ba26 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
use std::hash::Hash;
use std::sync::Arc;

use ecow::{eco_format, EcoString};
use typst_syntax::{Span, Spanned};
use typst_utils::{LazyHash, Numeric};

use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain};
use crate::introspection::Locator;
use crate::layout::{Abs, Axes, Frame, Length, Region, Size};
use crate::visualize::RelativeTo;
use crate::World;

/// A repeating pattern fill.
///
/// Typst supports the most common pattern type of tiled patterns, where a
/// pattern is repeated in a grid-like fashion, covering the entire area of an
/// element that is filled or stroked. The pattern is defined by a tile size and
/// a body defining the content of each cell. You can also add horizontal or
/// vertical spacing between the cells of the pattern.
///
/// # Examples
///
/// ```example
/// #let pat = pattern(size: (30pt, 30pt))[
///   #place(line(start: (0%, 0%), end: (100%, 100%)))
///   #place(line(start: (0%, 100%), end: (100%, 0%)))
/// ]
///
/// #rect(fill: pat, width: 100%, height: 60pt, stroke: 1pt)
/// ```
///
/// Patterns are also supported on text, but only when setting the
/// [relativeness]($pattern.relative) to either `{auto}` (the default value) or
/// `{"parent"}`. To create word-by-word or glyph-by-glyph patterns, you can
/// wrap the words or characters of your text in [boxes]($box) manually or
/// through a [show rule]($styling/#show-rules).
///
/// ```example
/// #let pat = pattern(
///   size: (30pt, 30pt),
///   relative: "parent",
///   square(
///     size: 30pt,
///     fill: gradient
///       .conic(..color.map.rainbow),
///   )
/// )
///
/// #set text(fill: pat)
/// #lorem(10)
/// ```
///
/// You can also space the elements further or closer apart using the
/// [`spacing`]($pattern.spacing) feature of the pattern. If the spacing
/// is lower than the size of the pattern, the pattern will overlap.
/// If it is higher, the pattern will have gaps of the same color as the
/// background of the pattern.
///
/// ```example
/// #let pat = pattern(
///   size: (30pt, 30pt),
///   spacing: (10pt, 10pt),
///   relative: "parent",
///   square(
///     size: 30pt,
///     fill: gradient
///      .conic(..color.map.rainbow),
///   ),
/// )
///
/// #rect(
///   width: 100%,
///   height: 60pt,
///   fill: pat,
/// )
/// ```
///
/// # Relativeness
/// The location of the starting point of the pattern is dependent on the
/// dimensions of a container. This container can either be the shape that it is
/// being painted on, or the closest surrounding container. This is controlled
/// by the `relative` argument of a pattern constructor. By default, patterns
/// are relative to the shape they are being painted on, unless the pattern is
/// applied on text, in which case they are relative to the closest ancestor
/// container.
///
/// Typst determines the ancestor container as follows:
/// - For shapes that are placed at the root/top level of the document, the
///   closest ancestor is the page itself.
/// - For other shapes, the ancestor is the innermost [`block`] or [`box`] that
///   contains the shape. This includes the boxes and blocks that are implicitly
///   created by show rules and elements. For example, a [`rotate`] will not
///   affect the parent of a gradient, but a [`grid`] will.
#[ty(scope, cast)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Pattern(Arc<Repr>);

/// Internal representation of [`Pattern`].
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct Repr {
    /// The pattern's rendered content.
    frame: LazyHash<Frame>,
    /// The pattern's tile size.
    size: Size,
    /// The pattern's tile spacing.
    spacing: Size,
    /// The pattern's relative transform.
    relative: Smart<RelativeTo>,
}

#[scope]
impl Pattern {
    /// Construct a new pattern.
    ///
    /// ```example
    /// #let pat = pattern(
    ///   size: (20pt, 20pt),
    ///   relative: "parent",
    ///   place(
    ///     dx: 5pt,
    ///     dy: 5pt,
    ///     rotate(45deg, square(
    ///       size: 5pt,
    ///       fill: black,
    ///     )),
    ///   ),
    /// )
    ///
    /// #rect(width: 100%, height: 60pt, fill: pat)
    /// ```
    #[func(constructor)]
    pub fn construct(
        engine: &mut Engine,
        /// The callsite span.
        span: Span,
        /// The bounding box of each cell of the pattern.
        #[named]
        #[default(Spanned::new(Smart::Auto, Span::detached()))]
        size: Spanned<Smart<Axes<Length>>>,
        /// The spacing between cells of the pattern.
        #[named]
        #[default(Spanned::new(Axes::splat(Length::zero()), Span::detached()))]
        spacing: Spanned<Axes<Length>>,
        /// The [relative placement](#relativeness) of the pattern.
        ///
        /// For an element placed at the root/top level of the document, the
        /// parent is the page itself. For other elements, the parent is the
        /// innermost block, box, column, grid, or stack that contains the
        /// element.
        #[named]
        #[default(Smart::Auto)]
        relative: Smart<RelativeTo>,
        /// The content of each cell of the pattern.
        body: Content,
    ) -> SourceResult<Pattern> {
        let size_span = size.span;
        if let Smart::Custom(size) = size.v {
            // Ensure that sizes are absolute.
            if !size.x.em.is_zero() || !size.y.em.is_zero() {
                bail!(size_span, "pattern tile size must be absolute");
            }

            // Ensure that sizes are non-zero and finite.
            if size.x.is_zero()
                || size.y.is_zero()
                || !size.x.is_finite()
                || !size.y.is_finite()
            {
                bail!(size_span, "pattern tile size must be non-zero and non-infinite");
            }
        }

        // Ensure that spacing is absolute.
        if !spacing.v.x.em.is_zero() || !spacing.v.y.em.is_zero() {
            bail!(spacing.span, "pattern tile spacing must be absolute");
        }

        // Ensure that spacing is finite.
        if !spacing.v.x.is_finite() || !spacing.v.y.is_finite() {
            bail!(spacing.span, "pattern tile spacing must be finite");
        }

        // The size of the frame
        let size = size.v.map(|l| l.map(|a| a.abs));
        let region = size.unwrap_or_else(|| Axes::splat(Abs::inf()));

        // Layout the pattern.
        let world = engine.world;
        let library = world.library();
        let locator = Locator::root();
        let styles = StyleChain::new(&library.styles);
        let pod = Region::new(region, Axes::splat(false));
        let mut frame =
            (engine.routines.layout_frame)(engine, &body, locator, styles, pod)?;

        // Set the size of the frame if the size is enforced.
        if let Smart::Custom(size) = size {
            frame.set_size(size);
        }

        // Check that the frame is non-zero.
        if frame.width().is_zero() || frame.height().is_zero() {
            bail!(
                span, "pattern tile size must be non-zero";
                hint: "try setting the size manually"
            );
        }

        Ok(Self(Arc::new(Repr {
            size: frame.size(),
            frame: LazyHash::new(frame),
            spacing: spacing.v.map(|l| l.abs),
            relative,
        })))
    }
}

impl Pattern {
    /// Set the relative placement of the pattern.
    pub fn with_relative(mut self, relative: RelativeTo) -> Self {
        if let Some(this) = Arc::get_mut(&mut self.0) {
            this.relative = Smart::Custom(relative);
        } else {
            self.0 = Arc::new(Repr {
                relative: Smart::Custom(relative),
                ..self.0.as_ref().clone()
            });
        }

        self
    }

    /// Return the frame of the pattern.
    pub fn frame(&self) -> &Frame {
        &self.0.frame
    }

    /// Return the size of the pattern in absolute units.
    pub fn size(&self) -> Size {
        self.0.size
    }

    /// Return the spacing of the pattern in absolute units.
    pub fn spacing(&self) -> Size {
        self.0.spacing
    }

    /// Returns the relative placement of the pattern.
    pub fn relative(&self) -> Smart<RelativeTo> {
        self.0.relative
    }

    /// Returns the relative placement of the pattern.
    pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo {
        self.0.relative.unwrap_or_else(|| {
            if on_text {
                RelativeTo::Parent
            } else {
                RelativeTo::Self_
            }
        })
    }
}

impl repr::Repr for Pattern {
    fn repr(&self) -> EcoString {
        let mut out =
            eco_format!("pattern(({}, {})", self.0.size.x.repr(), self.0.size.y.repr());

        if self.0.spacing.is_zero() {
            out.push_str(", spacing: (");
            out.push_str(&self.0.spacing.x.repr());
            out.push_str(", ");
            out.push_str(&self.0.spacing.y.repr());
            out.push(')');
        }

        out.push_str(", ..)");

        out
    }
}