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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
//! Immutable placement and compositing modifiers.
use super::*;
/// Snapshot modifiers shared by all values convertible into a [`Node`].
///
/// Placement, compositing, clipping and filters preserve immutable snapshots.
pub trait NodeModifiers: Into<Node> + Sized {
/// Layers three soft silhouettes behind a single untouched source snapshot.
fn layered_shadow(self, color: crate::Color, radius: f64, offset: crate::Vector2) -> Node {
super::elevation::shadow_stack(
self.into(),
color,
[(1.0, 0.2), (0.5, 0.3), (0.2, 0.5)]
.map(|(scale, opacity)| (radius * scale, offset, opacity)),
)
}
/// Layers contact, key and ambient soft shadows that scale with an [`Elevation`](super::elevation::Elevation).
fn elevated(self, elevation: super::elevation::Elevation) -> Node {
elevation.apply(self.into())
}
/// Repeatable physical gate drift, keyed only by an explicit frame and seed.
fn gate_weave(self, seed: u64, frame: u64, amount: f64) -> Node {
let random = super::sampling::shutter_random;
self.into()
.translate(
(random(seed ^ frame.wrapping_mul(3)) * 2.0 - 1.0) * amount,
(random(seed ^ frame.wrapping_mul(3).wrapping_add(1)) * 2.0 - 1.0) * amount,
)
.rotate(
(random(seed ^ frame.wrapping_mul(3).wrapping_add(2)) * 2.0 - 1.0) * amount * 0.001,
)
}
/// Places content next to the reference during compile-time layout.
fn next_to(self, reference: &Node, side: Side, gap: f64) -> Node {
self.into().next_to(reference, side, gap)
}
/// Aligns an edge with a compile-time measured reference snapshot.
fn align_to(self, reference: &Node, edge: Side) -> Node {
self.into().align_to(reference, edge)
}
/// Places the selected pivot at a parent-space point, before translation.
fn at(self, position: impl Into<Point>) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).placement.position = Some(position.into());
node
}
/// Adds parent-space translation, after scale and rotation.
fn translate(self, horizontal: f64, vertical: f64) -> Node {
let mut node = self.into();
let translation = &mut Arc::make_mut(&mut node.data).placement.translation;
translation.x += horizontal;
translation.y += vertical;
node
}
/// Adds clockwise radians about the pivot, after scale and before translation.
fn rotate(self, radians: f64) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).placement.rotation += radians;
node
}
/// Multiplies both scale factors; negative factors reflect and zero collapses.
fn scale(self, factor: f64) -> Node {
self.scale_xy(factor, factor)
}
/// Multiplies per-axis scale factors about the pivot.
fn scale_xy(self, horizontal: f64, vertical: f64) -> Node {
let mut node = self.into();
let scale = &mut Arc::make_mut(&mut node.data).placement.scale;
scale.x *= horizontal;
scale.y *= vertical;
node
}
/// Replaces the local-space pivot; defaults to the layout-bounds center.
fn pivot(self, pivot: Pivot) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).placement.pivot = pivot;
node
}
/// Replaces subtree opacity; invalid values are reported during conversion.
fn opacity(self, opacity: f64) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).opacity = opacity;
node
}
/// Blends the completed subtree once against its immediate parent backdrop.
///
/// Modes use the selected blend space and force isolation even at opacity one.
/// This does not change layout bounds.
///
/// ```
/// use motion_core::prelude::*;
/// use motion_core::{
/// DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
/// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
/// };
/// let tile = || Node::from(Rect::new(0.0, 0.0, 1.0, 1.0).fill(Color::WHITE)).opacity(0.5);
/// let scene = Node::group([tile(), tile().blend_mode(BlendMode::Multiply)]);
/// let prepared = scene.compile(
/// Size::new(1.0, 1.0),
/// PixelSize::new(1, 1),
/// ValidationLimits::default(),
/// )?;
/// # Ok::<(), motion_core::ValidationError>(())
/// ```
fn blend_mode(self, mode: BlendMode) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).blend_mode = mode;
node
}
/// Appends a source-only filter, preserving declaration order.
///
/// Filters process the completed subtree before this node's content mask,
/// clips, opacity and blend. Child effects complete before parent effects.
/// Layout bounds stay unchanged. Distances transform from local design units;
/// CPU preparation retains offscreen inputs and derives capture bounds.
///
/// ```
/// use motion_core::prelude::*;
/// use motion_core::{
/// DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
/// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
/// };
/// let node = Node::from(Rect::new(0.0, 0.0, 2.0, 2.0).fill(Color::WHITE))
/// .filter(Filter::Blur { sigma: 0.5 });
/// let output = node.compile(
/// Size::new(4.0, 4.0),
/// PixelSize::new(4, 4),
/// ValidationLimits::default(),
/// )?;
/// # Ok::<(), motion_core::ValidationError>(())
/// ```
fn filter(self, filter: crate::Filter) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).filters.push(filter);
node
}
/// Replaces the subtree's local path-coverage clip without changing layout.
///
/// Coverage multiplies the completed isolated subtree once, before group
/// opacity and blending. Nested masks multiply; this is not geometric path
/// intersection. Use a checked Path2D or a PathClip with an explicit rule.
///
/// ```
/// use motion_core::prelude::*;
/// use motion_core::{
/// DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
/// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
/// };
/// let tile = || Node::from(Rect::new(0.0, 0.0, 2.0, 1.0).fill(Color::WHITE));
/// let path = Path2D::from_rect(Rect::new(0.5, 0.0, 2.0, 1.0))?;
/// let node = Node::group([tile(), tile()]).clip(path);
/// let prepared = node.compile(
/// Size::new(2.0, 1.0),
/// PixelSize::new(2, 1),
/// ValidationLimits::default(),
/// )?;
/// # Ok::<(), motion_core::ValidationError>(())
/// ```
fn clip(self, shape: impl Into<crate::ShapeClip>) -> Node {
self.clip_with(shape, crate::ClipMode::Coverage)
}
/// Clips with explicit coverage multiplication or geometric intersection.
fn clip_with(self, shape: impl Into<crate::ShapeClip>, mode: crate::ClipMode) -> Node {
let shape = shape.into();
let mut node = self.into();
let data = Arc::make_mut(&mut node.data);
match mode {
crate::ClipMode::Coverage => {
let path = match shape.geometry {
crate::drawing::ClipGeometry::Path(path) => path,
crate::drawing::ClipGeometry::Rectangle(bounds) => {
match crate::Path2D::from_rect(bounds) {
Ok(path) => Arc::new(path),
Err(error) => return super::transition::diagnostic(error),
}
}
};
data.clip = Some(crate::PathClip {
path,
transform: shape.transform,
rule: shape.rule,
});
}
crate::ClipMode::Geometric => match shape.geometry {
crate::drawing::ClipGeometry::Rectangle(bounds) => {
data.shape_clip = None;
data.rect_clip = Some(crate::RectClip {
bounds,
transform: shape.transform,
});
}
geometry => {
data.rect_clip = None;
data.shape_clip = Some(crate::ShapeClip {
geometry,
transform: shape.transform,
rule: shape.rule,
});
}
},
}
node
}
/// Replaces the local geometric rectangle clip without changing layout.
///
/// Nested rectangles intersect in device space before coverage quantization,
/// including rotated/reflected rectangles. Repeating an edge does not fade
/// it again. The completed subtree is clipped once before opacity/blending.
/// Path clips and content masks remain independent multiplicative coverage.
/// RectClip allows a separate local transform. Invalid hidden clips fail
/// validation; zero area is empty.
///
/// ```
/// use motion_core::prelude::*;
/// use motion_core::{
/// DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
/// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
/// };
/// let rect = Rect::new(0.5, 0.0, 2.0, 1.0);
/// let inner = Node::from(Rect::new(0.0, 0.0, 2.0, 1.0).fill(Color::WHITE))
/// .clip_with(rect, motion_core::ClipMode::Geometric);
/// let frame = Node::group([inner])
/// .clip_with(rect, motion_core::ClipMode::Geometric)
/// .compile(
/// Size::new(2.0, 1.0),
/// PixelSize::new(2, 1),
/// ValidationLimits::default(),
/// )?;
/// # Ok::<(), motion_core::ValidationError>(())
/// ```
///
/// Replaces the geometric Shape snapshot without changing layout.
///
/// Shapes and rectangles intersect before coverage quantization. NonZero
/// is the default; ShapeClip exposes an independent rule and local transform.
/// Path coverage masks and content masks still multiply independently.
/// Curves use the preparation tolerance, not a pixel-area error guarantee.
///
/// ```
/// use motion_core::prelude::*;
/// use motion_core::{
/// DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
/// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
/// };
/// let mut triangle = Path2D::new();
/// triangle
/// .move_to(Point::ORIGIN)
/// .line_to(Point::new(1.0, 0.0))
/// .line_to(Point::new(0.0, 1.0));
/// let inner = Node::from(Rect::new(0.0, 0.0, 1.0, 1.0).fill(Color::WHITE))
/// .clip_with(triangle.clone(), motion_core::ClipMode::Geometric);
/// let frame = Node::group([inner])
/// .clip_with(triangle, motion_core::ClipMode::Geometric)
/// .compile(
/// Size::new(1.0, 1.0),
/// PixelSize::new(1, 1),
/// ValidationLimits::default(),
/// )?;
/// # Ok::<(), motion_core::ValidationError>(())
/// ```
///
/// Replaces the subtree's alpha or linear-luminance content mask.
///
/// Mask artwork uses the source's local coordinate system and a transparent
/// isolated backdrop. It does not contribute layout bounds. The completed
/// mask scales content once, before path clipping, opacity and blending.
/// Inverted modes complement clamped coverage, not color or alpha first.
/// Empty or transparent mask regions then preserve existing source pixels;
/// they do not create source artwork or enlarge its capture bounds.
///
/// ```
/// use motion_core::prelude::*;
/// use motion_core::{
/// DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
/// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
/// };
/// let tile = |color| Node::from(Rect::new(0.0, 0.0, 1.0, 1.0).fill(color));
/// let node = tile(Color::WHITE).mask(
/// tile(Color::from_hex(0xff0000).with_alpha(0.5)),
/// MaskMode::Luminance,
/// );
/// let frame = node.compile(
/// Size::new(1.0, 1.0),
/// PixelSize::new(1, 1),
/// ValidationLimits::default(),
/// )?;
/// # Ok::<(), motion_core::ValidationError>(())
/// ```
///
/// ```
/// use motion_core::prelude::*;
/// use motion_core::{
/// DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
/// MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
/// };
/// let source = Node::from(Rect::new(1.0, 0.0, 3.0, 1.0).fill(Color::WHITE));
/// let mask = Node::from(Rect::new(1.0, 0.0, 2.0, 1.0).fill(Color::WHITE));
/// let frame = source.mask(mask, MaskMode::InvertedAlpha).compile(
/// Size::new(4.0, 1.0),
/// PixelSize::new(4, 1),
/// ValidationLimits::default(),
/// )?;
/// # Ok::<(), motion_core::ValidationError>(())
/// ```
fn mask(self, mask: impl Into<Node>, mode: crate::drawing::MaskMode) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).mask = Some(Box::new(NodeMask {
node: mask.into(),
mode,
}));
node
}
/// Replaces sibling-local stacking order; ties retain insertion order.
fn z_index(self, index: i32) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).z_index = index;
node
}
/// Removes the snapshot entirely when false, rather than evaluating it hidden.
fn visible(self, visible: bool) -> Node {
if visible { self.into() } else { Node::empty() }
}
/// Attaches a diagnostic name without changing artwork or draw order.
fn named(self, name: &str) -> Node {
let mut node = self.into();
Arc::make_mut(&mut node.data).name = Some(Arc::from(name));
node
}
}
impl<Value: Into<Node>> NodeModifiers for Value {}