Skip to main content

motion_core/authoring/
modifiers.rs

1//! Immutable placement and compositing modifiers.
2
3use super::*;
4
5/// Snapshot modifiers shared by all values convertible into a [`Node`].
6///
7/// Placement, compositing, clipping and filters preserve immutable snapshots.
8pub trait NodeModifiers: Into<Node> + Sized {
9    /// Layers three soft silhouettes behind a single untouched source snapshot.
10    fn layered_shadow(self, color: crate::Color, radius: f64, offset: crate::Vector2) -> Node {
11        super::elevation::shadow_stack(
12            self.into(),
13            color,
14            [(1.0, 0.2), (0.5, 0.3), (0.2, 0.5)]
15                .map(|(scale, opacity)| (radius * scale, offset, opacity)),
16        )
17    }
18
19    /// Layers contact, key and ambient soft shadows that scale with an [`Elevation`](super::elevation::Elevation).
20    fn elevated(self, elevation: super::elevation::Elevation) -> Node {
21        elevation.apply(self.into())
22    }
23
24    /// Repeatable physical gate drift, keyed only by an explicit frame and seed.
25    fn gate_weave(self, seed: u64, frame: u64, amount: f64) -> Node {
26        let random = super::sampling::shutter_random;
27        self.into()
28            .translate(
29                (random(seed ^ frame.wrapping_mul(3)) * 2.0 - 1.0) * amount,
30                (random(seed ^ frame.wrapping_mul(3).wrapping_add(1)) * 2.0 - 1.0) * amount,
31            )
32            .rotate(
33                (random(seed ^ frame.wrapping_mul(3).wrapping_add(2)) * 2.0 - 1.0) * amount * 0.001,
34            )
35    }
36
37    /// Places content next to the reference during compile-time layout.
38    fn next_to(self, reference: &Node, side: Side, gap: f64) -> Node {
39        self.into().next_to(reference, side, gap)
40    }
41
42    /// Aligns an edge with a compile-time measured reference snapshot.
43    fn align_to(self, reference: &Node, edge: Side) -> Node {
44        self.into().align_to(reference, edge)
45    }
46
47    /// Places the selected pivot at a parent-space point, before translation.
48    fn at(self, position: impl Into<Point>) -> Node {
49        let mut node = self.into();
50        Arc::make_mut(&mut node.data).placement.position = Some(position.into());
51        node
52    }
53
54    /// Adds parent-space translation, after scale and rotation.
55    fn translate(self, horizontal: f64, vertical: f64) -> Node {
56        let mut node = self.into();
57        let translation = &mut Arc::make_mut(&mut node.data).placement.translation;
58        translation.x += horizontal;
59        translation.y += vertical;
60        node
61    }
62
63    /// Adds clockwise radians about the pivot, after scale and before translation.
64    fn rotate(self, radians: f64) -> Node {
65        let mut node = self.into();
66        Arc::make_mut(&mut node.data).placement.rotation += radians;
67        node
68    }
69
70    /// Multiplies both scale factors; negative factors reflect and zero collapses.
71    fn scale(self, factor: f64) -> Node {
72        self.scale_xy(factor, factor)
73    }
74
75    /// Multiplies per-axis scale factors about the pivot.
76    fn scale_xy(self, horizontal: f64, vertical: f64) -> Node {
77        let mut node = self.into();
78        let scale = &mut Arc::make_mut(&mut node.data).placement.scale;
79        scale.x *= horizontal;
80        scale.y *= vertical;
81        node
82    }
83
84    /// Replaces the local-space pivot; defaults to the layout-bounds center.
85    fn pivot(self, pivot: Pivot) -> Node {
86        let mut node = self.into();
87        Arc::make_mut(&mut node.data).placement.pivot = pivot;
88        node
89    }
90
91    /// Replaces subtree opacity; invalid values are reported during conversion.
92    fn opacity(self, opacity: f64) -> Node {
93        let mut node = self.into();
94        Arc::make_mut(&mut node.data).opacity = opacity;
95        node
96    }
97
98    /// Blends the completed subtree once against its immediate parent backdrop.
99    ///
100    /// Modes use the selected blend space and force isolation even at opacity one.
101    /// This does not change layout bounds.
102    ///
103    /// ```
104    /// use motion_core::prelude::*;
105    /// use motion_core::{
106    ///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
107    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
108    /// };
109    /// let tile = || Node::from(Rect::new(0.0, 0.0, 1.0, 1.0).fill(Color::WHITE)).opacity(0.5);
110    /// let scene = Node::group([tile(), tile().blend_mode(BlendMode::Multiply)]);
111    /// let prepared = scene.compile(
112    ///     Size::new(1.0, 1.0),
113    ///     PixelSize::new(1, 1),
114    ///     ValidationLimits::default(),
115    /// )?;
116    /// # Ok::<(), motion_core::ValidationError>(())
117    /// ```
118    fn blend_mode(self, mode: BlendMode) -> Node {
119        let mut node = self.into();
120        Arc::make_mut(&mut node.data).blend_mode = mode;
121        node
122    }
123
124    /// Appends a source-only filter, preserving declaration order.
125    ///
126    /// Filters process the completed subtree before this node's content mask,
127    /// clips, opacity and blend. Child effects complete before parent effects.
128    /// Layout bounds stay unchanged. Distances transform from local design units;
129    /// CPU preparation retains offscreen inputs and derives capture bounds.
130    ///
131    /// ```
132    /// use motion_core::prelude::*;
133    /// use motion_core::{
134    ///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
135    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
136    /// };
137    /// let node = Node::from(Rect::new(0.0, 0.0, 2.0, 2.0).fill(Color::WHITE))
138    ///     .filter(Filter::Blur { sigma: 0.5 });
139    /// let output = node.compile(
140    ///     Size::new(4.0, 4.0),
141    ///     PixelSize::new(4, 4),
142    ///     ValidationLimits::default(),
143    /// )?;
144    /// # Ok::<(), motion_core::ValidationError>(())
145    /// ```
146    fn filter(self, filter: crate::Filter) -> Node {
147        let mut node = self.into();
148        Arc::make_mut(&mut node.data).filters.push(filter);
149        node
150    }
151
152    /// Replaces the subtree's local path-coverage clip without changing layout.
153    ///
154    /// Coverage multiplies the completed isolated subtree once, before group
155    /// opacity and blending. Nested masks multiply; this is not geometric path
156    /// intersection. Use a checked Path2D or a PathClip with an explicit rule.
157    ///
158    /// ```
159    /// use motion_core::prelude::*;
160    /// use motion_core::{
161    ///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
162    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
163    /// };
164    /// let tile = || Node::from(Rect::new(0.0, 0.0, 2.0, 1.0).fill(Color::WHITE));
165    /// let path = Path2D::from_rect(Rect::new(0.5, 0.0, 2.0, 1.0))?;
166    /// let node = Node::group([tile(), tile()]).clip(path);
167    /// let prepared = node.compile(
168    ///     Size::new(2.0, 1.0),
169    ///     PixelSize::new(2, 1),
170    ///     ValidationLimits::default(),
171    /// )?;
172    /// # Ok::<(), motion_core::ValidationError>(())
173    /// ```
174    fn clip(self, shape: impl Into<crate::ShapeClip>) -> Node {
175        self.clip_with(shape, crate::ClipMode::Coverage)
176    }
177
178    /// Clips with explicit coverage multiplication or geometric intersection.
179    fn clip_with(self, shape: impl Into<crate::ShapeClip>, mode: crate::ClipMode) -> Node {
180        let shape = shape.into();
181        let mut node = self.into();
182        let data = Arc::make_mut(&mut node.data);
183        match mode {
184            crate::ClipMode::Coverage => {
185                let path = match shape.geometry {
186                    crate::drawing::ClipGeometry::Path(path) => path,
187                    crate::drawing::ClipGeometry::Rectangle(bounds) => {
188                        match crate::Path2D::from_rect(bounds) {
189                            Ok(path) => Arc::new(path),
190                            Err(error) => return super::transition::diagnostic(error),
191                        }
192                    }
193                };
194                data.clip = Some(crate::PathClip {
195                    path,
196                    transform: shape.transform,
197                    rule: shape.rule,
198                });
199            }
200            crate::ClipMode::Geometric => match shape.geometry {
201                crate::drawing::ClipGeometry::Rectangle(bounds) => {
202                    data.shape_clip = None;
203                    data.rect_clip = Some(crate::RectClip {
204                        bounds,
205                        transform: shape.transform,
206                    });
207                }
208                geometry => {
209                    data.rect_clip = None;
210                    data.shape_clip = Some(crate::ShapeClip {
211                        geometry,
212                        transform: shape.transform,
213                        rule: shape.rule,
214                    });
215                }
216            },
217        }
218        node
219    }
220
221    /// Replaces the local geometric rectangle clip without changing layout.
222    ///
223    /// Nested rectangles intersect in device space before coverage quantization,
224    /// including rotated/reflected rectangles. Repeating an edge does not fade
225    /// it again. The completed subtree is clipped once before opacity/blending.
226    /// Path clips and content masks remain independent multiplicative coverage.
227    /// RectClip allows a separate local transform. Invalid hidden clips fail
228    /// validation; zero area is empty.
229    ///
230    /// ```
231    /// use motion_core::prelude::*;
232    /// use motion_core::{
233    ///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
234    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
235    /// };
236    /// let rect = Rect::new(0.5, 0.0, 2.0, 1.0);
237    /// let inner = Node::from(Rect::new(0.0, 0.0, 2.0, 1.0).fill(Color::WHITE))
238    ///     .clip_with(rect, motion_core::ClipMode::Geometric);
239    /// let frame = Node::group([inner])
240    ///     .clip_with(rect, motion_core::ClipMode::Geometric)
241    ///     .compile(
242    ///         Size::new(2.0, 1.0),
243    ///         PixelSize::new(2, 1),
244    ///         ValidationLimits::default(),
245    ///     )?;
246    /// # Ok::<(), motion_core::ValidationError>(())
247    /// ```
248    ///
249    /// Replaces the geometric Shape snapshot without changing layout.
250    ///
251    /// Shapes and rectangles intersect before coverage quantization. NonZero
252    /// is the default; ShapeClip exposes an independent rule and local transform.
253    /// Path coverage masks and content masks still multiply independently.
254    /// Curves use the preparation tolerance, not a pixel-area error guarantee.
255    ///
256    /// ```
257    /// use motion_core::prelude::*;
258    /// use motion_core::{
259    ///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
260    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
261    /// };
262    /// let mut triangle = Path2D::new();
263    /// triangle
264    ///     .move_to(Point::ORIGIN)
265    ///     .line_to(Point::new(1.0, 0.0))
266    ///     .line_to(Point::new(0.0, 1.0));
267    /// let inner = Node::from(Rect::new(0.0, 0.0, 1.0, 1.0).fill(Color::WHITE))
268    ///     .clip_with(triangle.clone(), motion_core::ClipMode::Geometric);
269    /// let frame = Node::group([inner])
270    ///     .clip_with(triangle, motion_core::ClipMode::Geometric)
271    ///     .compile(
272    ///         Size::new(1.0, 1.0),
273    ///         PixelSize::new(1, 1),
274    ///         ValidationLimits::default(),
275    ///     )?;
276    /// # Ok::<(), motion_core::ValidationError>(())
277    /// ```
278    ///
279    /// Replaces the subtree's alpha or linear-luminance content mask.
280    ///
281    /// Mask artwork uses the source's local coordinate system and a transparent
282    /// isolated backdrop. It does not contribute layout bounds. The completed
283    /// mask scales content once, before path clipping, opacity and blending.
284    /// Inverted modes complement clamped coverage, not color or alpha first.
285    /// Empty or transparent mask regions then preserve existing source pixels;
286    /// they do not create source artwork or enlarge its capture bounds.
287    ///
288    /// ```
289    /// use motion_core::prelude::*;
290    /// use motion_core::{
291    ///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
292    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
293    /// };
294    /// let tile = |color| Node::from(Rect::new(0.0, 0.0, 1.0, 1.0).fill(color));
295    /// let node = tile(Color::WHITE).mask(
296    ///     tile(Color::from_hex(0xff0000).with_alpha(0.5)),
297    ///     MaskMode::Luminance,
298    /// );
299    /// let frame = node.compile(
300    ///     Size::new(1.0, 1.0),
301    ///     PixelSize::new(1, 1),
302    ///     ValidationLimits::default(),
303    /// )?;
304    /// # Ok::<(), motion_core::ValidationError>(())
305    /// ```
306    ///
307    /// ```
308    /// use motion_core::prelude::*;
309    /// use motion_core::{
310    ///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
311    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
312    /// };
313    /// let source = Node::from(Rect::new(1.0, 0.0, 3.0, 1.0).fill(Color::WHITE));
314    /// let mask = Node::from(Rect::new(1.0, 0.0, 2.0, 1.0).fill(Color::WHITE));
315    /// let frame = source.mask(mask, MaskMode::InvertedAlpha).compile(
316    ///     Size::new(4.0, 1.0),
317    ///     PixelSize::new(4, 1),
318    ///     ValidationLimits::default(),
319    /// )?;
320    /// # Ok::<(), motion_core::ValidationError>(())
321    /// ```
322    fn mask(self, mask: impl Into<Node>, mode: crate::drawing::MaskMode) -> Node {
323        let mut node = self.into();
324        Arc::make_mut(&mut node.data).mask = Some(Box::new(NodeMask {
325            node: mask.into(),
326            mode,
327        }));
328        node
329    }
330
331    /// Replaces sibling-local stacking order; ties retain insertion order.
332    fn z_index(self, index: i32) -> Node {
333        let mut node = self.into();
334        Arc::make_mut(&mut node.data).z_index = index;
335        node
336    }
337
338    /// Removes the snapshot entirely when false, rather than evaluating it hidden.
339    fn visible(self, visible: bool) -> Node {
340        if visible { self.into() } else { Node::empty() }
341    }
342
343    /// Attaches a diagnostic name without changing artwork or draw order.
344    fn named(self, name: &str) -> Node {
345        let mut node = self.into();
346        Arc::make_mut(&mut node.data).name = Some(Arc::from(name));
347        node
348    }
349}
350
351impl<Value: Into<Node>> NodeModifiers for Value {}