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