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 {}