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, ResourceStore, 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, ResourceStore, 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, ResourceStore, 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 crate::IntoShapeClip) -> 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 crate::IntoShapeClip, mode: crate::ClipMode) -> Node {
180        let shape = match shape.into_shape_clip() {
181            Ok(shape) => shape,
182            Err(error) => return super::transition::diagnostic(error),
183        };
184        let mut node = self.into();
185        let data = Arc::make_mut(&mut node.data);
186        match mode {
187            crate::ClipMode::Coverage => {
188                let path = match shape.geometry {
189                    crate::drawing::ClipGeometry::Path(path) => path,
190                    crate::drawing::ClipGeometry::Rectangle(bounds) => {
191                        match crate::Path2D::from_rect(bounds) {
192                            Ok(path) => Arc::new(path),
193                            Err(error) => return super::transition::diagnostic(error),
194                        }
195                    }
196                };
197                data.clip = Some(crate::PathClip {
198                    path,
199                    transform: shape.transform,
200                    rule: shape.rule,
201                });
202            }
203            crate::ClipMode::Geometric => match shape.geometry {
204                crate::drawing::ClipGeometry::Rectangle(bounds) => {
205                    data.shape_clip = None;
206                    data.rect_clip = Some(crate::RectClip {
207                        bounds,
208                        transform: shape.transform,
209                    });
210                }
211                geometry => {
212                    data.rect_clip = None;
213                    data.shape_clip = Some(crate::ShapeClip {
214                        geometry,
215                        transform: shape.transform,
216                        rule: shape.rule,
217                    });
218                }
219            },
220        }
221        node
222    }
223
224    /// Replaces the local geometric rectangle clip without changing layout.
225    ///
226    /// Nested rectangles intersect in device space before coverage quantization,
227    /// including rotated/reflected rectangles. Repeating an edge does not fade
228    /// it again. The completed subtree is clipped once before opacity/blending.
229    /// Path clips and content masks remain independent multiplicative coverage.
230    /// RectClip allows a separate local transform. Invalid hidden clips fail
231    /// validation; zero area is empty.
232    ///
233    /// ```
234    /// use motion_core::prelude::*;
235    /// use motion_core::{
236    ///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
237    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
238    /// };
239    /// let rect = Rect::new(0.5, 0.0, 2.0, 1.0);
240    /// let inner = Node::from(Rect::new(0.0, 0.0, 2.0, 1.0).fill(Color::WHITE))
241    ///     .clip_with(rect, motion_core::ClipMode::Geometric);
242    /// let frame = Node::group([inner])
243    ///     .clip_with(rect, motion_core::ClipMode::Geometric)
244    ///     .compile(
245    ///         Size::new(2.0, 1.0),
246    ///         PixelSize::new(2, 1),
247    ///         ValidationLimits::default(),
248    ///     )?;
249    /// # Ok::<(), motion_core::ValidationError>(())
250    /// ```
251    ///
252    /// Replaces the geometric Shape snapshot without changing layout.
253    ///
254    /// Shapes and rectangles intersect before coverage quantization. NonZero
255    /// is the default; ShapeClip exposes an independent rule and local transform.
256    /// Path coverage masks and content masks still multiply independently.
257    /// Curves use the preparation tolerance, not a pixel-area error guarantee.
258    ///
259    /// ```
260    /// use motion_core::prelude::*;
261    /// use motion_core::{
262    ///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
263    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
264    /// };
265    /// let triangle = Path2D::builder()
266    ///     .move_to(Point::ORIGIN)
267    ///     .line_to(Point::new(1.0, 0.0))
268    ///     .line_to(Point::new(0.0, 1.0))
269    ///     .build()?;
270    /// let inner = Node::from(Rect::new(0.0, 0.0, 1.0, 1.0).fill(Color::WHITE))
271    ///     .clip_with(triangle.clone(), motion_core::ClipMode::Geometric);
272    /// let frame = Node::group([inner])
273    ///     .clip_with(triangle, motion_core::ClipMode::Geometric)
274    ///     .compile(
275    ///         Size::new(1.0, 1.0),
276    ///         PixelSize::new(1, 1),
277    ///         ValidationLimits::default(),
278    ///     )?;
279    /// # Ok::<(), motion_core::ValidationError>(())
280    /// ```
281    ///
282    /// Replaces the subtree's alpha or linear-luminance content mask.
283    ///
284    /// Mask artwork uses the source's local coordinate system and a transparent
285    /// isolated backdrop. It does not contribute layout bounds. The completed
286    /// mask scales content once, before path clipping, opacity and blending.
287    /// Inverted modes complement clamped coverage, not color or alpha first.
288    /// Empty or transparent mask regions then preserve existing source pixels;
289    /// they do not create source artwork or enlarge its capture bounds.
290    ///
291    /// ```
292    /// use motion_core::prelude::*;
293    /// use motion_core::{
294    ///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
295    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
296    /// };
297    /// let tile = |color| Node::from(Rect::new(0.0, 0.0, 1.0, 1.0).fill(color));
298    /// let node = tile(Color::WHITE).mask(
299    ///     tile(Color::from_hex(0xff0000).with_alpha(0.5)),
300    ///     MaskMode::Luminance,
301    /// );
302    /// let frame = node.compile(
303    ///     Size::new(1.0, 1.0),
304    ///     PixelSize::new(1, 1),
305    ///     ValidationLimits::default(),
306    /// )?;
307    /// # Ok::<(), motion_core::ValidationError>(())
308    /// ```
309    ///
310    /// ```
311    /// use motion_core::prelude::*;
312    /// use motion_core::{
313    ///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
314    ///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
315    /// };
316    /// let source = Node::from(Rect::new(1.0, 0.0, 3.0, 1.0).fill(Color::WHITE));
317    /// let mask = Node::from(Rect::new(1.0, 0.0, 2.0, 1.0).fill(Color::WHITE));
318    /// let frame = source.mask(mask, MaskMode::InvertedAlpha).compile(
319    ///     Size::new(4.0, 1.0),
320    ///     PixelSize::new(4, 1),
321    ///     ValidationLimits::default(),
322    /// )?;
323    /// # Ok::<(), motion_core::ValidationError>(())
324    /// ```
325    fn mask(self, mask: impl Into<Node>, mode: crate::drawing::MaskMode) -> Node {
326        let mut node = self.into();
327        Arc::make_mut(&mut node.data).mask = Some(Box::new(NodeMask {
328            node: mask.into(),
329            mode,
330        }));
331        node
332    }
333
334    /// Replaces sibling-local stacking order; ties retain insertion order.
335    fn z_index(self, index: i32) -> Node {
336        let mut node = self.into();
337        Arc::make_mut(&mut node.data).z_index = index;
338        node
339    }
340
341    /// Removes the snapshot entirely when false, rather than evaluating it hidden.
342    fn visible(self, visible: bool) -> Node {
343        if visible { self.into() } else { Node::empty() }
344    }
345
346    /// Attaches a diagnostic name without changing artwork or draw order.
347    fn named(self, name: &str) -> Node {
348        let mut node = self.into();
349        Arc::make_mut(&mut node.data).name = Some(Arc::from(name));
350        node
351    }
352}
353
354impl<Value: Into<Node>> NodeModifiers for Value {}