1use kurbo::{Affine, BezPath, ParamCurveNearest, Point, Shape as KurboShape};
9use renamite_animation::{
10 Angle, Animated, AnimatedTransform, EasingHandle, Frame, Interpolation, Tween,
11};
12use renamite_geometry::{VectorPath, dash_bez_path, offset_bez_path};
13pub use renamite_text::TextAlign;
14use serde::de::{Deserializer, Error as DeError, Visitor};
15use serde::{Deserialize, Serialize};
16use slotmap::{SlotMap, new_key_type};
17
18new_key_type! {
19 pub struct NodeId;
20 pub struct CompId;
21 pub struct AssetId;
22}
23
24pub type NodeMap = SlotMap<NodeId, Node>;
25pub type CompMap = SlotMap<CompId, Composition>;
26pub type AssetMap = SlotMap<AssetId, Asset>;
27
28#[derive(Clone, Serialize, Deserialize)]
29pub struct Document {
30 pub format_version: u32,
31 pub compositions: CompMap,
32 pub nodes: NodeMap,
33 pub assets: AssetMap,
34
35 #[serde(default)]
37 pub asset_order: Vec<AssetId>,
38
39 pub main: CompId,
40}
41
42#[derive(Clone, Serialize, Deserialize)]
43pub struct Composition {
44 pub name: String,
45 pub size: (u32, u32),
46 pub rate: renamite_animation::FrameRate,
47 pub range: (Frame, Frame),
48 pub children: Vec<NodeId>,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53pub struct Node {
54 pub name: String,
55 pub parent: Option<NodeId>,
56 pub children: Vec<NodeId>,
57 pub visible: bool,
58 pub locked: bool,
59 pub transform: AnimatedTransform,
60 pub opacity: Animated<f64>,
61 pub kind: NodeKind,
62}
63
64impl Node {
65 pub fn new(name: impl Into<String>, kind: NodeKind) -> Self {
66 Self {
67 name: name.into(),
68 parent: None,
69 children: Vec::new(),
70 visible: true,
71 locked: false,
72 transform: AnimatedTransform::identity(),
73 opacity: Animated::new(1.0),
74 kind,
75 }
76 }
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize)]
80pub enum NodeKind {
81 Group,
82 Layer(LayerProps),
83 Shape(ShapeKind),
84 Style(StyleKind),
85 Modifier(ModifierKind),
86 Text(TextNode),
87 Image(AssetId),
88 Precomp { comp: CompId, time_map: TimeMap },
89 Mask(MaskProps),
90}
91
92#[derive(Clone, Debug, Serialize, Deserialize)]
93pub struct LayerProps {
94 pub in_frame: Frame,
95 pub out_frame: Frame,
96 pub time_stretch: f64,
97 pub blend: BlendMode,
98}
99
100impl Default for LayerProps {
101 fn default() -> Self {
102 Self {
103 in_frame: Frame(0),
104 out_frame: Frame(i64::MAX / 2),
105 time_stretch: 1.0,
106 blend: BlendMode::Normal,
107 }
108 }
109}
110
111#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
114pub struct CompoundPath {
115 pub contours: Vec<Animated<VectorPath>>,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
121pub enum ShapeKind {
122 Path(Animated<VectorPath>),
123 Rect {
124 pos: Animated<glam::DVec2>,
125 size: Animated<glam::DVec2>,
126 rounded: Animated<f64>,
127 },
128 Ellipse {
129 pos: Animated<glam::DVec2>,
130 size: Animated<glam::DVec2>,
131 },
132 Star {
133 pos: Animated<glam::DVec2>,
134 points: Animated<f64>,
135 inner_r: Animated<f64>,
136 outer_r: Animated<f64>,
137 roundness: Animated<f64>,
138 kind: StarKind,
139 },
140 Polygon {
141 pos: Animated<glam::DVec2>,
142 points: Animated<f64>,
143 outer_r: Animated<f64>,
144 roundness: Animated<f64>,
145 },
146
147 CompoundPath(CompoundPath),
149}
150
151impl CompoundPath {
152 pub fn to_bez_path(&self, frame: f64) -> BezPath {
154 let mut result = BezPath::new();
155 for contour in &self.contours {
156 result.extend(
157 contour
158 .value_at(frame)
159 .to_bez_path()
160 .elements()
161 .iter()
162 .copied(),
163 );
164 }
165 result
166 }
167}
168
169#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
171pub struct GradientStop {
172 pub offset: f64,
173 pub color: Color,
174}
175
176#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
178pub struct GradientStops(pub Vec<GradientStop>);
179
180impl Default for GradientStops {
181 fn default() -> Self {
182 Self(vec![
183 GradientStop {
184 offset: 0.0,
185 color: Color::rgba(1.0, 1.0, 1.0, 1.0),
186 },
187 GradientStop {
188 offset: 1.0,
189 color: Color::rgba(0.0, 0.0, 0.0, 1.0),
190 },
191 ])
192 }
193}
194
195impl GradientStops {
196 pub fn sample(&self, t: f64) -> Color {
198 let t = t.clamp(0.0, 1.0);
199 let stops = &self.0;
200 if stops.is_empty() {
201 return Color::BLACK;
202 }
203 if t <= stops[0].offset {
204 return stops[0].color;
205 }
206 for w in stops.windows(2) {
207 let (a, b) = (&w[0], &w[1]);
208 if t <= b.offset {
209 let span = (b.offset - a.offset).max(1e-9);
210 let u = ((t - a.offset) / span).clamp(0.0, 1.0);
211 return Color::rgba(
212 a.color.r + (b.color.r - a.color.r) * u,
213 a.color.g + (b.color.g - a.color.g) * u,
214 a.color.b + (b.color.b - a.color.b) * u,
215 a.color.a + (b.color.a - a.color.a) * u,
216 );
217 }
218 }
219 stops.last().unwrap().color
220 }
221}
222
223impl Tween for GradientStops {
224 fn tween(a: &Self, b: &Self, t: f64) -> Self {
225 if a.0.len() != b.0.len() {
226 return if t < 1.0 { a.clone() } else { b.clone() };
227 }
228 Self(
229 a.0.iter()
230 .zip(&b.0)
231 .map(|(x, y)| GradientStop {
232 offset: x.offset + (y.offset - x.offset) * t,
233 color: Color::rgba(
234 x.color.r + (y.color.r - x.color.r) * t,
235 x.color.g + (y.color.g - x.color.g) * t,
236 x.color.b + (y.color.b - x.color.b) * t,
237 x.color.a + (y.color.a - x.color.a) * t,
238 ),
239 })
240 .collect(),
241 )
242 }
243}
244
245#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
246pub enum GradientKind {
247 Linear,
248 Radial,
249}
250
251#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
254pub struct Gradient {
255 pub kind: GradientKind,
256 pub start: Animated<glam::DVec2>,
258 pub end: Animated<glam::DVec2>,
260 pub stops: Animated<GradientStops>,
261}
262
263#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
266pub enum StylePaint {
267 Solid { color: Animated<Color> },
268 Gradient(Gradient),
269}
270
271impl StylePaint {
272 pub fn solid(color: Color) -> Self {
273 Self::Solid {
274 color: Animated::new(color),
275 }
276 }
277
278 pub fn linear(start: glam::DVec2, end: glam::DVec2, stops: GradientStops) -> Self {
279 Self::Gradient(Gradient {
280 kind: GradientKind::Linear,
281 start: Animated::new(start),
282 end: Animated::new(end),
283 stops: Animated::new(stops),
284 })
285 }
286
287 pub fn radial(center: glam::DVec2, end: glam::DVec2, stops: GradientStops) -> Self {
288 Self::Gradient(Gradient {
289 kind: GradientKind::Radial,
290 start: Animated::new(center),
291 end: Animated::new(end),
292 stops: Animated::new(stops),
293 })
294 }
295
296 pub fn sample(&self, frame: f64) -> ScenePaint {
298 match self {
299 StylePaint::Solid { color } => ScenePaint::Solid(color.value_at(frame)),
300 StylePaint::Gradient(g) => {
301 let start = g.start.value_at(frame);
302 let end = g.end.value_at(frame);
303 let stops = g.stops.value_at(frame);
304 match g.kind {
305 GradientKind::Linear => ScenePaint::LinearGradient { start, end, stops },
306 GradientKind::Radial => ScenePaint::RadialGradient {
307 center: start,
308 end,
309 stops,
310 },
311 }
312 }
313 }
314 }
315
316 pub fn snapshot(&self, frame: f64) -> Self {
321 match self {
322 StylePaint::Solid { color } => StylePaint::solid(color.value_at(frame)),
323 StylePaint::Gradient(gradient) => StylePaint::Gradient(Gradient {
324 kind: gradient.kind,
325 start: Animated::new(gradient.start.value_at(frame)),
326 end: Animated::new(gradient.end.value_at(frame)),
327 stops: Animated::new(gradient.stops.value_at(frame)),
328 }),
329 }
330 }
331
332 pub fn set_base_color(&mut self, color: Color) {
337 match self {
338 StylePaint::Solid { color: animated } => {
339 animated.base = color;
340 animated.keyframes.clear();
341 }
342 StylePaint::Gradient(gradient) => {
343 gradient.start.keyframes.clear();
344 gradient.end.keyframes.clear();
345 gradient.stops.keyframes.clear();
346
347 if let Some(first) = gradient.stops.base.0.first_mut() {
348 first.color = color;
349 } else {
350 gradient
351 .stops
352 .base
353 .0
354 .push(GradientStop { offset: 0.0, color });
355 }
356 }
357 }
358 }
359}
360
361impl Tween for StylePaint {
362 fn tween(a: &Self, b: &Self, t: f64) -> Self {
363 match (a, b) {
364 (StylePaint::Solid { color: ca }, StylePaint::Solid { color: cb }) => {
365 StylePaint::Solid {
366 color: Animated::new(Tween::tween(&ca.base, &cb.base, t)),
367 }
368 }
369 (StylePaint::Gradient(ga), StylePaint::Gradient(gb)) => {
370 StylePaint::Gradient(Gradient {
371 kind: ga.kind,
372 start: Animated::new(Tween::tween(&ga.start.base, &gb.start.base, t)),
373 end: Animated::new(Tween::tween(&ga.end.base, &gb.end.base, t)),
374 stops: Animated::new(Tween::tween(&ga.stops.base, &gb.stops.base, t)),
375 })
376 }
377 _ => {
378 if t < 1.0 {
379 a.clone()
380 } else {
381 b.clone()
382 }
383 }
384 }
385 }
386}
387
388impl StyleKind {
389 pub fn swap_paint(&mut self, paint: StylePaint) -> StylePaint {
391 match self {
392 StyleKind::Fill { paint: p, .. } | StyleKind::Stroke { paint: p, .. } => {
393 std::mem::replace(p, paint)
394 }
395 }
396 }
397
398 pub fn paint(&self) -> &StylePaint {
399 match self {
400 StyleKind::Fill { paint, .. } | StyleKind::Stroke { paint, .. } => paint,
401 }
402 }
403}
404
405impl StylePaint {
406 pub fn base_color(&self) -> Color {
409 match self {
410 StylePaint::Solid { color } => color.base,
411 StylePaint::Gradient(g) => g
412 .stops
413 .base
414 .0
415 .first()
416 .map(|s| s.color)
417 .unwrap_or(Color::BLACK),
418 }
419 }
420}
421
422#[derive(Clone, Debug, PartialEq, Serialize)]
423pub enum StyleKind {
424 Fill {
425 paint: StylePaint,
426 rule: FillRule,
427 },
428 Stroke {
429 paint: StylePaint,
430 width: Animated<f64>,
431 cap: StrokeCap,
432 join: StrokeJoin,
433 dash: Option<AnimatedDash>,
434 },
435}
436
437#[derive(Default)]
438struct StyleCompatContent {
439 paint: Option<StylePaint>,
440 color: Option<Animated<Color>>,
441 width: Option<Animated<f64>>,
442 cap: Option<StrokeCap>,
443 join: Option<StrokeJoin>,
444 dash: Option<AnimatedDash>,
445 rule: Option<FillRule>,
446}
447
448impl<'de> Deserialize<'de> for StyleKind {
449 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
450 where
451 D: Deserializer<'de>,
452 {
453 #[derive(Deserialize)]
454 enum StyleTag {
455 Fill,
456 Stroke,
457 }
458
459 struct StyleKindVisitor;
460 impl<'de> Visitor<'de> for StyleKindVisitor {
461 type Value = StyleKind;
462
463 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
464 f.write_str("a Fill or Stroke style")
465 }
466
467 fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
468 where
469 A: serde::de::EnumAccess<'de>,
470 {
471 use serde::de::VariantAccess as _;
472 struct ContentVisitor {
473 fill: bool,
474 }
475 impl<'de> Visitor<'de> for ContentVisitor {
476 type Value = StyleCompatContent;
477 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
478 f.write_str("style variant fields")
479 }
480 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
481 where
482 A: serde::de::MapAccess<'de>,
483 {
484 use serde::de::Error as _;
485 let mut content = StyleCompatContent::default();
486 while let Some(key) = map.next_key::<String>()? {
487 match key.as_str() {
488 "paint" => content.paint = Some(map.next_value()?),
489 "color" => content.color = Some(map.next_value()?),
490 "width" => content.width = Some(map.next_value()?),
491 "cap" => content.cap = Some(map.next_value()?),
492 "join" => content.join = Some(map.next_value()?),
493 "dash" => {
494 content.dash = map.next_value::<Option<AnimatedDash>>()?
495 }
496 "rule" => content.rule = Some(map.next_value()?),
497 other => {
498 return Err(A::Error::unknown_field(
499 other,
500 &["paint", "color", "width", "cap", "join", "dash", "rule"],
501 ));
502 }
503 }
504 }
505 Ok(content)
506 }
507 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
508 where
509 A: serde::de::SeqAccess<'de>,
510 {
511 use serde::de::Error as _;
512 let mut content = StyleCompatContent {
515 paint: Some(
516 seq.next_element()?
517 .ok_or_else(|| A::Error::invalid_length(0, &"paint"))?,
518 ),
519 ..Default::default()
520 };
521 if self.fill {
522 content.rule = Some(
523 seq.next_element()?
524 .ok_or_else(|| A::Error::invalid_length(1, &"rule"))?,
525 );
526 } else {
527 content.width = Some(
528 seq.next_element()?
529 .ok_or_else(|| A::Error::invalid_length(1, &"width"))?,
530 );
531 content.cap = Some(
532 seq.next_element()?
533 .ok_or_else(|| A::Error::invalid_length(2, &"cap"))?,
534 );
535 content.join = Some(
536 seq.next_element()?
537 .ok_or_else(|| A::Error::invalid_length(3, &"join"))?,
538 );
539 content.dash = seq
540 .next_element::<Option<AnimatedDash>>()?
541 .ok_or_else(|| A::Error::invalid_length(4, &"dash"))?;
542 }
543 Ok(content)
544 }
545 }
546 let (tag, content) = data.variant::<StyleTag>()?;
547 let content = match tag {
548 StyleTag::Fill => {
549 content.struct_variant(&["paint", "rule"], ContentVisitor { fill: true })?
550 }
551 StyleTag::Stroke => content.struct_variant(
552 &["paint", "width", "cap", "join", "dash"],
553 ContentVisitor { fill: false },
554 )?,
555 };
556 let paint = match content.paint {
557 Some(p) => p,
558 None => match content.color {
559 Some(color) => StylePaint::Solid { color },
560 None => return Err(A::Error::missing_field("paint")),
561 },
562 };
563 match tag {
564 StyleTag::Fill => Ok(StyleKind::Fill {
565 paint,
566 rule: content
567 .rule
568 .ok_or_else(|| A::Error::missing_field("rule"))?,
569 }),
570 StyleTag::Stroke => Ok(StyleKind::Stroke {
571 paint,
572 width: content
573 .width
574 .ok_or_else(|| A::Error::missing_field("width"))?,
575 cap: content.cap.ok_or_else(|| A::Error::missing_field("cap"))?,
576 join: content
577 .join
578 .ok_or_else(|| A::Error::missing_field("join"))?,
579 dash: content.dash,
580 }),
581 }
582 }
583 }
584
585 deserializer.deserialize_enum("StyleKind", &["Fill", "Stroke"], StyleKindVisitor)
586 }
587}
588
589fn animated_one() -> Animated<f64> {
591 Animated::new(1.0)
592}
593
594#[derive(Clone, Debug, Serialize, Deserialize)]
595pub enum ModifierKind {
596 TrimPath {
597 start: Animated<f64>,
598 end: Animated<f64>,
599 offset: Animated<f64>,
600 #[serde(default)]
601 mode: TrimMode,
602 },
603 Repeater {
604 copies: Animated<f64>,
605 offset: Animated<f64>,
606 transform: AnimatedTransform,
607 #[serde(default = "animated_one")]
609 start_opacity: Animated<f64>,
610 #[serde(default = "animated_one")]
612 end_opacity: Animated<f64>,
613 },
614 RoundCorners {
615 radius: Animated<f64>,
616 },
617 OffsetPath {
618 amount: Animated<f64>,
619 },
620 ZigZag {
621 amplitude: Animated<f64>,
622 frequency: Animated<f64>,
623 #[serde(default)]
625 smooth: bool,
626 },
627 PuckerBloat {
628 amount: Animated<f64>,
630 },
631}
632
633#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
635pub enum TrimMode {
636 #[default]
638 Individually,
639 Simultaneously,
641}
642
643#[derive(Clone, Debug, Serialize, Deserialize)]
644pub struct TimeMap {
645 pub offset: Frame,
646 pub stretch: f64,
647}
648
649fn default_text_size() -> Animated<f64> {
650 Animated::new(48.0)
651}
652
653#[derive(Clone, Debug, Serialize, Deserialize)]
654pub struct TextNode {
655 pub text: String,
656 #[serde(default = "default_text_size")]
659 pub size: Animated<f64>,
660 #[serde(default)]
661 pub align: TextAlign,
662 #[serde(default)]
664 pub font: Option<String>,
665}
666
667#[derive(Clone, Debug, Serialize, Deserialize)]
668pub struct MaskProps {
669 pub inverted: bool,
670
671 #[serde(default)]
675 pub shape: ShapeKind,
676}
677
678impl Default for ShapeKind {
679 fn default() -> Self {
680 ShapeKind::Path(Animated::new(renamite_geometry::VectorPath::default()))
681 }
682}
683
684#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
685pub struct Color {
686 pub r: f64,
687 pub g: f64,
688 pub b: f64,
689 pub a: f64,
690}
691
692impl Color {
693 pub const BLACK: Self = Self {
694 r: 0.0,
695 g: 0.0,
696 b: 0.0,
697 a: 1.0,
698 };
699 pub const WHITE: Self = Self {
700 r: 1.0,
701 g: 1.0,
702 b: 1.0,
703 a: 1.0,
704 };
705 pub fn rgba(r: f64, g: f64, b: f64, a: f64) -> Self {
706 Self { r, g, b, a }
707 }
708}
709
710impl Tween for Color {
711 fn tween(a: &Self, b: &Self, t: f64) -> Self {
712 Self {
713 r: a.r + (b.r - a.r) * t,
714 g: a.g + (b.g - a.g) * t,
715 b: a.b + (b.b - a.b) * t,
716 a: a.a + (b.a - a.a) * t,
717 }
718 }
719}
720
721#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
722pub enum FillRule {
723 #[default]
724 NonZero,
725 EvenOdd,
726}
727#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
728pub enum StrokeCap {
729 Butt,
730 Round,
731 Square,
732}
733#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
734pub enum StrokeJoin {
735 Miter,
736 Round,
737 Bevel,
738}
739#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
740pub struct AnimatedDash {
741 pub dashes: Vec<Animated<f64>>,
742 pub offset: Animated<f64>,
743}
744#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
745pub enum StarKind {
746 Star,
747 Burst,
748}
749#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
750pub enum BlendMode {
751 Normal,
752 Multiply,
753 Screen,
754}
755#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
756pub enum Asset {
757 Image(ImageAsset),
758 Font(FontAsset),
759}
760
761fn default_true() -> bool {
762 true
763}
764
765#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
768pub struct ImageAsset {
769 pub name: String,
770 pub mime: String,
771
772 pub bytes: Vec<u8>,
774
775 pub width: u32,
777 pub height: u32,
778
779 #[serde(default = "default_true")]
781 pub srgb: bool,
782}
783
784#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
787pub struct FontAsset {
788 pub name: String,
790 pub family: String,
792 pub bytes: Vec<u8>,
794}
795
796#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
797pub struct Scene {
798 pub items: Vec<SceneItem>,
799 pub clips: Vec<ClipPath>,
800}
801
802#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
803pub struct SceneItem {
804 pub path: BezPath,
806 pub node: NodeId,
808 pub style: NodeId,
811 pub paint: ScenePaint,
815 pub kind: PaintKind,
816 pub opacity: f64,
817
818 #[serde(default)]
820 pub clips: Vec<u32>,
821
822 pub blend: BlendMode,
823}
824
825#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
826pub enum PaintKind {
827 Fill(FillRule),
828 Stroke(StrokeSample),
829}
830
831#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
832pub struct StrokeSample {
833 pub width: f64,
834 pub cap: StrokeCap,
835 pub join: StrokeJoin,
836 pub dash: Option<DashSample>,
837}
838#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
839pub struct DashSample {
840 pub dashes: Vec<f64>,
841 pub offset: f64,
842}
843
844#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
847pub enum ScenePaint {
848 Solid(Color),
849 LinearGradient {
850 start: glam::DVec2,
851 end: glam::DVec2,
852 stops: GradientStops,
853 },
854 RadialGradient {
855 center: glam::DVec2,
856 end: glam::DVec2,
857 stops: GradientStops,
858 },
859 Image {
860 asset: AssetId,
861
862 width: u32,
864 height: u32,
865
866 affine: [f64; 6],
868
869 tint: Color,
871 },
872}
873
874impl ScenePaint {
875 pub fn color_at(&self, p: glam::DVec2) -> Color {
877 match self {
878 ScenePaint::Solid(c) => *c,
879 ScenePaint::Image { tint, .. } => *tint,
880 ScenePaint::LinearGradient { start, end, stops } => {
881 let d = *end - *start;
882 let len2 = d.length_squared().max(1e-12);
883 let t = ((p - *start).dot(d) / len2).clamp(0.0, 1.0);
884 stops.sample(t)
885 }
886 ScenePaint::RadialGradient { center, end, stops } => {
887 let r = (*end - *center).length().max(1e-12);
888 let t = ((p - *center).length() / r).clamp(0.0, 1.0);
889 stops.sample(t)
890 }
891 }
892 }
893}
894
895#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
896pub struct ClipPath {
897 pub path: BezPath,
898 #[serde(default)]
899 pub rule: FillRule,
900}
901
902#[derive(Clone, Debug, Default, PartialEq)]
905pub struct Overrides {
906 pub values: std::collections::HashMap<(NodeId, PropPath), Value>,
907}
908
909impl Overrides {
910 pub fn set(&mut self, id: NodeId, prop: PropPath, v: Value) {
911 self.values.insert((id, prop), v);
912 }
913 pub fn get(&self, id: NodeId, prop: &str) -> Option<&Value> {
915 self.values.get(&(id, PropPath::new(prop)))
916 }
917 pub fn is_empty(&self) -> bool {
918 self.values.is_empty()
919 }
920 pub fn clear(&mut self) {
921 self.values.clear();
922 }
923}
924
925fn ov_f64(ov: &Overrides, id: NodeId, prop: &str, dflt: f64) -> f64 {
926 match ov.get(id, prop) {
927 Some(Value::F64(x)) => *x,
928 _ => dflt,
929 }
930}
931fn ov_vec2(ov: &Overrides, id: NodeId, prop: &str, dflt: glam::DVec2) -> glam::DVec2 {
932 match ov.get(id, prop) {
933 Some(Value::DVec2(x)) => *x,
934 _ => dflt,
935 }
936}
937fn ov_angle(ov: &Overrides, id: NodeId, prop: &str, dflt: f64) -> f64 {
938 match ov.get(id, prop) {
939 Some(Value::Angle(a)) => a.0,
940 Some(Value::F64(x)) => *x,
941 _ => dflt,
942 }
943}
944fn ov_color(ov: &Overrides, id: NodeId, prop: &str, dflt: Color) -> Color {
945 match ov.get(id, prop) {
946 Some(Value::Color(c)) => *c,
947 _ => dflt,
948 }
949}
950
951fn sample_transform(
952 n: &Node,
953 id: NodeId,
954 frame: f64,
955 ov: &Overrides,
956) -> renamite_animation::TransformSample {
957 let mut ts = n.transform.sample(frame);
958 if !ov.is_empty() {
959 ts.anchor = ov_vec2(ov, id, "transform.anchor", ts.anchor);
960 ts.position = ov_vec2(ov, id, "transform.position", ts.position);
961 ts.scale = ov_vec2(ov, id, "transform.scale", ts.scale);
962 ts.rotation_deg = ov_angle(ov, id, "transform.rotation", ts.rotation_deg);
963 ts.skew = ov_f64(ov, id, "transform.skew", ts.skew);
964 ts.skew_axis = ov_f64(ov, id, "transform.skew_axis", ts.skew_axis);
965 }
966 ts
967}
968
969pub fn fill_style_for(doc: &Document, shape: NodeId) -> Option<NodeId> {
973 let mut scope = doc.locate(shape).map(|(p, _)| p)?;
974 loop {
975 let children: Vec<NodeId> = match scope {
976 Parent::Comp(c) => doc.compositions.get(c)?.children.clone(),
977 Parent::Node(p) => doc.nodes.get(p)?.children.clone(),
978 };
979 if let Some(fill) = children.iter().rev().find(|id| {
980 matches!(
981 doc.nodes.get(**id).map(|n| &n.kind),
982 Some(NodeKind::Style(StyleKind::Fill { .. }))
983 )
984 }) {
985 return Some(*fill);
986 }
987 match scope {
988 Parent::Comp(_) => return None,
989 Parent::Node(p) => scope = doc.locate(p).map(|(parent, _)| parent)?,
990 }
991 }
992}
993
994pub fn stroke_style_for(doc: &Document, shape: NodeId) -> Option<NodeId> {
996 let mut scope = doc.locate(shape).map(|(p, _)| p)?;
997 loop {
998 let children: Vec<NodeId> = match scope {
999 Parent::Comp(c) => doc.compositions.get(c)?.children.clone(),
1000 Parent::Node(p) => doc.nodes.get(p)?.children.clone(),
1001 };
1002 if let Some(stroke) = children.iter().rev().find(|id| {
1003 matches!(
1004 doc.nodes.get(**id).map(|n| &n.kind),
1005 Some(NodeKind::Style(StyleKind::Stroke { .. }))
1006 )
1007 }) {
1008 return Some(*stroke);
1009 }
1010 match scope {
1011 Parent::Comp(_) => return None,
1012 Parent::Node(p) => scope = doc.locate(p).map(|(parent, _)| parent)?,
1013 }
1014 }
1015}
1016
1017pub fn node_affine(doc: &Document, id: NodeId, frame: f64) -> Affine {
1022 let Some(n) = doc.nodes.get(id) else {
1023 return Affine::IDENTITY;
1024 };
1025 affine_of(&sample_transform(n, id, frame, &Overrides::default()))
1026}
1027
1028fn linear_affine_of(sample: &renamite_animation::TransformSample) -> Affine {
1029 let axis = sample.skew_axis.to_radians();
1030 let skew = Affine::rotate(axis)
1031 * Affine::skew(sample.skew.to_radians().tan(), 0.0)
1032 * Affine::rotate(-axis);
1033 Affine::rotate(sample.rotation_deg.to_radians())
1034 * skew
1035 * Affine::scale_non_uniform(sample.scale.x / 100.0, sample.scale.y / 100.0)
1036}
1037
1038fn affine_of(sample: &renamite_animation::TransformSample) -> Affine {
1039 Affine::translate((sample.position.x, sample.position.y))
1040 * linear_affine_of(sample)
1041 * Affine::translate((-sample.anchor.x, -sample.anchor.y))
1042}
1043
1044#[derive(Clone, Copy, Debug)]
1046pub struct NodeTransformContext {
1047 pub parent_world: Affine,
1049
1050 pub linear: Affine,
1053
1054 pub local: Affine,
1056
1057 pub world: Affine,
1059
1060 pub frame: f64,
1062
1063 pub position: glam::DVec2,
1065
1066 pub anchor: glam::DVec2,
1068
1069 pub pivot_world: glam::DVec2,
1071}
1072
1073fn node_effective_frame(node: &Node, incoming_frame: f64) -> f64 {
1074 match &node.kind {
1075 NodeKind::Layer(layer) => {
1076 (incoming_frame - layer.in_frame.0 as f64) / layer.time_stretch.max(1e-9)
1077 + layer.in_frame.0 as f64
1078 }
1079
1080 _ => incoming_frame,
1081 }
1082}
1083
1084pub fn node_transform_context(
1091 doc: &Document,
1092 id: NodeId,
1093 root_frame: f64,
1094) -> Option<NodeTransformContext> {
1095 let mut chain = Vec::new();
1096 let mut current = id;
1097
1098 loop {
1099 chain.push(current);
1100
1101 let node = doc.nodes.get(current)?;
1102
1103 let Some(parent) = node.parent else {
1104 break;
1105 };
1106
1107 current = parent;
1108 }
1109
1110 chain.reverse();
1111
1112 let mut parent_world = Affine::IDENTITY;
1113 let mut frame = root_frame;
1114
1115 for current in chain {
1116 let node = doc.nodes.get(current)?;
1117 let effective = node_effective_frame(node, frame);
1118 let sample = node.transform.sample(effective);
1119 let linear = linear_affine_of(&sample);
1120 let local = affine_of(&sample);
1121
1122 if current == id {
1123 let pivot = parent_world * Point::new(sample.position.x, sample.position.y);
1124
1125 return Some(NodeTransformContext {
1126 parent_world,
1127 linear,
1128 local,
1129 world: parent_world * local,
1130 frame: effective,
1131 position: sample.position,
1132 anchor: sample.anchor,
1133 pivot_world: glam::DVec2::new(pivot.x, pivot.y),
1134 });
1135 }
1136
1137 parent_world *= local;
1138 frame = effective;
1139 }
1140
1141 None
1142}
1143
1144const SHAPE_TOL: f64 = 0.1;
1145
1146pub fn shape_path(kind: &ShapeKind, id: NodeId, frame: f64, ov: &Overrides) -> BezPath {
1149 match kind {
1150 ShapeKind::Path(p) => {
1151 if let Some(Value::Path(p)) = ov.get(id, "shape.path") {
1152 return p.to_bez_path();
1153 }
1154 p.value_at(frame).to_bez_path()
1155 }
1156 ShapeKind::Rect { pos, size, rounded } => {
1157 let c = ov_vec2(ov, id, "shape.pos", pos.value_at(frame));
1158 let s = ov_vec2(ov, id, "shape.size", size.value_at(frame));
1159 let r = kurbo::Rect::from_center_size((c.x, c.y), (s.x.abs(), s.y.abs()));
1160 let radius = ov_f64(ov, id, "shape.rounded", rounded.value_at(frame));
1161 if radius > 1e-9 {
1162 kurbo::RoundedRect::from_rect(r, radius).to_path(SHAPE_TOL)
1163 } else {
1164 r.to_path(SHAPE_TOL)
1165 }
1166 }
1167 ShapeKind::Ellipse { pos, size } => {
1168 let c = ov_vec2(ov, id, "shape.pos", pos.value_at(frame));
1169 let s = ov_vec2(ov, id, "shape.size", size.value_at(frame));
1170 kurbo::Ellipse::new((c.x, c.y), (s.x.abs() / 2.0, s.y.abs() / 2.0), 0.0)
1171 .to_path(SHAPE_TOL)
1172 }
1173 ShapeKind::Star {
1174 pos,
1175 points,
1176 inner_r,
1177 outer_r,
1178 roundness,
1179 kind,
1180 } => {
1181 let pts = ov_f64(ov, id, "shape.points", points.value_at(frame))
1182 .round()
1183 .max(3.0) as usize;
1184 let outer = ov_f64(ov, id, "shape.outer_r", outer_r.value_at(frame));
1185 let inner = match kind {
1186 StarKind::Burst => None,
1188 StarKind::Star => Some(ov_f64(ov, id, "shape.inner_r", inner_r.value_at(frame))),
1189 };
1190 star_path(
1191 ov_vec2(ov, id, "shape.pos", pos.value_at(frame)),
1192 pts,
1193 inner,
1194 outer,
1195 ov_f64(ov, id, "shape.roundness", roundness.value_at(frame)).max(0.0),
1196 )
1197 }
1198 ShapeKind::Polygon {
1199 pos,
1200 points,
1201 outer_r,
1202 roundness,
1203 } => star_path(
1204 ov_vec2(ov, id, "shape.pos", pos.value_at(frame)),
1205 ov_f64(ov, id, "shape.points", points.value_at(frame))
1206 .round()
1207 .max(3.0) as usize,
1208 None,
1209 ov_f64(ov, id, "shape.outer_r", outer_r.value_at(frame)),
1210 ov_f64(ov, id, "shape.roundness", roundness.value_at(frame)).max(0.0),
1211 ),
1212 ShapeKind::CompoundPath(compound) => compound.to_bez_path(frame),
1213 }
1214}
1215
1216fn star_path(
1219 center: glam::DVec2,
1220 points: usize,
1221 inner: Option<f64>,
1222 outer: f64,
1223 roundness: f64,
1224) -> BezPath {
1225 let n = if inner.is_some() { points * 2 } else { points };
1226 let mut anchors = Vec::with_capacity(n);
1227 for k in 0..n {
1228 let ang = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * k as f64 / n as f64;
1229 let r = match inner {
1230 Some(ir) if k % 2 == 1 => ir,
1231 _ => outer,
1232 };
1233 anchors.push(renamite_geometry::Anchor::corner(glam::DVec2::new(
1234 center.x + r * ang.cos(),
1235 center.y + r * ang.sin(),
1236 )));
1237 }
1238 let sharp = renamite_geometry::VectorPath {
1239 closed: true,
1240 anchors,
1241 };
1242 if roundness <= 1e-9 {
1243 return sharp.to_bez_path();
1244 }
1245 sharp.round_corners(roundness).to_bez_path()
1246}
1247
1248pub fn evaluate(doc: &Document, comp: CompId, frame: f64) -> Scene {
1249 evaluate_with(doc, comp, frame, &Overrides::default())
1250}
1251
1252fn mask_shape_path(shape: &ShapeKind, id: NodeId, frame: f64, ov: &Overrides) -> BezPath {
1253 shape_path(shape, id, frame, ov)
1254}
1255
1256fn inverted_clip_path(scope_world: &BezPath, mask_world: &BezPath) -> ClipPath {
1257 let mut path = scope_world.clone();
1258 path.extend(mask_world.clone());
1259 ClipPath {
1260 path,
1261 rule: FillRule::EvenOdd,
1262 }
1263}
1264
1265pub fn evaluate_with(doc: &Document, comp: CompId, frame: f64, ov: &Overrides) -> Scene {
1266 let mut scene = Scene::default();
1267 if let Some(c) = doc.compositions.get(comp) {
1268 let scope = kurbo::Rect::new(0.0, 0.0, c.size.0 as f64, c.size.1 as f64);
1269 eval_group(
1270 doc,
1271 &c.children,
1272 frame,
1273 Affine::IDENTITY,
1274 1.0,
1275 BlendMode::Normal,
1276 &mut scene,
1277 0,
1278 ov,
1279 scope,
1280 &[],
1281 &[],
1282 );
1283 }
1284 scene
1285}
1286
1287const MAX_DEPTH: u32 = 32; fn eval_group(
1290 doc: &Document,
1291 children: &[NodeId],
1292 frame: f64,
1293 tf: Affine,
1294 opacity: f64,
1295 blend: BlendMode,
1296 scene: &mut Scene,
1297 depth: u32,
1298 ov: &Overrides,
1299 scope_rect: kurbo::Rect,
1300 inherited_clips: &[u32],
1301 seed_paths: &[ShapeEntry],
1302) {
1303 if depth > MAX_DEPTH {
1304 return;
1305 }
1306
1307 let mut paths: Vec<ShapeEntry> = seed_paths.to_vec();
1309 for &id in children {
1310 let Some(n) = doc.nodes.get(id) else { continue };
1311 if !n.visible {
1312 continue;
1313 }
1314 match &n.kind {
1315 NodeKind::Shape(s) => {
1316 let ntf = affine_of(&sample_transform(n, id, frame, ov));
1317 paths.push(ShapeEntry {
1318 node: id,
1319 affine: ntf,
1320 opacity: 1.0,
1321 path: tf * ntf * shape_path(s, id, frame, ov),
1322 });
1323 }
1324 NodeKind::Text(t) => {
1325 let ntf = affine_of(&sample_transform(n, id, frame, ov));
1326 let size = ov_f64(ov, id, "text.size", t.size.value_at(frame)).max(0.1);
1327 let outline = if let Some((_, font)) =
1329 t.font.as_deref().and_then(|f| doc.font_asset_for_family(f))
1330 {
1331 renamite_text::shape_text_from_bytes(&font.bytes, &t.text, size, t.align)
1332 .unwrap_or_else(|_| {
1333 renamite_text::shape_text_default(&t.text, size, t.align)
1334 })
1335 } else {
1336 renamite_text::shape_text_default(&t.text, size, t.align)
1337 };
1338 paths.push(ShapeEntry {
1339 node: id,
1340 affine: ntf,
1341 opacity: 1.0,
1342 path: tf * ntf * outline,
1343 });
1344 }
1345 NodeKind::Modifier(m) => apply_modifier(m, id, frame, ov, &mut paths),
1346 NodeKind::Mask(_) => {}
1347 _ => {}
1348 }
1349 }
1350
1351 let mut active: Vec<Vec<u32>> = Vec::with_capacity(children.len());
1353 let mut acc = inherited_clips.to_vec();
1354 for &id in children {
1355 active.push(acc.clone());
1356 let Some(n) = doc.nodes.get(id) else { continue };
1357 if !n.visible {
1358 continue;
1359 }
1360 if let NodeKind::Mask(mask) = &n.kind {
1361 let local = affine_of(&sample_transform(n, id, frame, ov));
1362 let world_mask = tf * local * mask_shape_path(&mask.shape, id, frame, ov);
1363 let clip = if mask.inverted {
1364 inverted_clip_path(&(tf * scope_rect.to_path(0.1)), &world_mask)
1365 } else {
1366 ClipPath {
1367 path: world_mask,
1368 rule: FillRule::NonZero,
1369 }
1370 };
1371 scene.clips.push(clip);
1372 acc.push((scene.clips.len() - 1) as u32);
1373 }
1374 }
1375
1376 for (i, &id) in children.iter().enumerate().rev() {
1379 let Some(n) = doc.nodes.get(id) else { continue };
1380 if !n.visible {
1381 continue;
1382 }
1383 let node_op =
1384 opacity * ov_f64(ov, id, "opacity", n.opacity.value_at(frame)).clamp(0.0, 1.0);
1385 let clips = &active[i];
1386 match &n.kind {
1387 NodeKind::Mask(_) => {}
1388 NodeKind::Group => {
1389 let ntf = tf * affine_of(&sample_transform(n, id, frame, ov));
1390 eval_group(
1391 doc,
1392 &n.children,
1393 frame,
1394 ntf,
1395 node_op,
1396 blend,
1397 scene,
1398 depth + 1,
1399 ov,
1400 scope_rect,
1401 clips,
1402 &[],
1403 );
1404 }
1405 NodeKind::Layer(lp) => {
1406 if frame < lp.in_frame.0 as f64 || frame > lp.out_frame.0 as f64 {
1407 continue;
1408 }
1409 let lf = (frame - lp.in_frame.0 as f64) / lp.time_stretch.max(1e-9)
1410 + lp.in_frame.0 as f64;
1411 let ntf = tf * affine_of(&sample_transform(n, id, lf, ov));
1412 eval_group(
1413 doc,
1414 &n.children,
1415 lf,
1416 ntf,
1417 node_op,
1418 lp.blend,
1419 scene,
1420 depth + 1,
1421 ov,
1422 scope_rect,
1423 clips,
1424 &[],
1425 );
1426 }
1427 NodeKind::Image(asset_id) => {
1428 let Some(asset) = doc.image_asset(*asset_id) else {
1429 continue;
1430 };
1431
1432 let node_transform = affine_of(&sample_transform(n, id, frame, ov));
1433 let full_transform = tf * node_transform;
1434
1435 let local_rect =
1436 kurbo::Rect::new(0.0, 0.0, asset.width as f64, asset.height as f64);
1437
1438 let world_path = full_transform * local_rect.to_path(0.1);
1439
1440 scene.items.push(SceneItem {
1441 path: world_path,
1442 node: id,
1443 style: id,
1444 paint: ScenePaint::Image {
1445 asset: *asset_id,
1446 width: asset.width,
1447 height: asset.height,
1448 affine: full_transform.as_coeffs(),
1449 tint: Color::WHITE,
1450 },
1451 kind: PaintKind::Fill(FillRule::NonZero),
1452 opacity: node_op,
1453 clips: clips.to_vec(),
1454 blend,
1455 });
1456 }
1457 NodeKind::Precomp { comp, time_map } => {
1458 let ntf = tf * affine_of(&sample_transform(n, id, frame, ov));
1459 let cf = (frame - time_map.offset.0 as f64) / time_map.stretch.max(1e-9);
1460 if let Some(c) = doc.compositions.get(*comp) {
1461 let pre_scope = kurbo::Rect::new(0.0, 0.0, c.size.0 as f64, c.size.1 as f64);
1462 eval_group(
1463 doc,
1464 &c.children,
1465 cf,
1466 ntf,
1467 node_op,
1468 blend,
1469 scene,
1470 depth + 1,
1471 ov,
1472 pre_scope,
1473 clips,
1474 &[],
1475 );
1476 }
1477 }
1478 NodeKind::Style(st) => emit_style(st, id, frame, ov, &paths, node_op, blend, clips, scene),
1479 NodeKind::Shape(_) | NodeKind::Text(_) if !n.children.is_empty() => {
1480 let seeds: Vec<ShapeEntry> =
1481 paths.iter().filter(|e| e.node == id).cloned().collect();
1482 eval_group(
1483 doc,
1484 &n.children,
1485 frame,
1486 tf,
1487 node_op,
1488 blend,
1489 scene,
1490 depth + 1,
1491 ov,
1492 scope_rect,
1493 clips,
1494 &seeds,
1495 );
1496 }
1497 _ => {}
1498 }
1499 }
1500}
1501
1502#[derive(Clone)]
1506struct ShapeEntry {
1507 node: NodeId,
1508 affine: Affine,
1509 opacity: f64,
1510 path: BezPath,
1511}
1512
1513fn apply_modifier(
1514 m: &ModifierKind,
1515 id: NodeId,
1516 frame: f64,
1517 ov: &Overrides,
1518 paths: &mut Vec<ShapeEntry>,
1519) {
1520 match m {
1521 ModifierKind::Repeater {
1522 copies,
1523 offset,
1524 transform,
1525 start_opacity,
1526 end_opacity,
1527 } => {
1528 let count = ov_f64(ov, id, "repeater.copies", copies.value_at(frame))
1529 .round()
1530 .max(0.0) as usize;
1531 let off = ov_f64(ov, id, "repeater.offset", offset.value_at(frame));
1532 let so = ov_f64(
1533 ov,
1534 id,
1535 "repeater.start_opacity",
1536 start_opacity.value_at(frame),
1537 )
1538 .clamp(0.0, 1.0);
1539 let eo =
1540 ov_f64(ov, id, "repeater.end_opacity", end_opacity.value_at(frame)).clamp(0.0, 1.0);
1541 let mut ts = transform.sample(frame);
1542 ts.position = ov_vec2(ov, id, "repeater.transform.position", ts.position);
1543 ts.scale = ov_vec2(ov, id, "repeater.transform.scale", ts.scale);
1544 ts.rotation_deg = ov_angle(ov, id, "repeater.transform.rotation", ts.rotation_deg);
1545 ts.anchor = ov_vec2(ov, id, "repeater.transform.anchor", ts.anchor);
1546 ts.skew = ov_f64(ov, id, "repeater.transform.skew", ts.skew);
1547 ts.skew_axis = ov_f64(ov, id, "repeater.transform.skew_axis", ts.skew_axis);
1548 let step = affine_of(&ts);
1549 let original = std::mem::take(paths);
1550 let n = count.max(1);
1551 for i in 0..n {
1552 let t = if n <= 1 {
1554 0.0
1555 } else {
1556 i as f64 / (n - 1) as f64
1557 };
1558 let copy_opacity = so + (eo - so) * t;
1559
1560 let mut a = Affine::IDENTITY;
1561 let reps = (i as f64 + off).max(0.0) as usize;
1562 for _ in 0..reps {
1563 a *= step;
1564 }
1565 for e in &original {
1566 paths.push(ShapeEntry {
1567 node: e.node,
1568 affine: e.affine,
1569 opacity: e.opacity * copy_opacity,
1570 path: a * e.path.clone(),
1571 });
1572 }
1573 }
1574 }
1575 ModifierKind::TrimPath {
1576 start,
1577 end,
1578 offset,
1579 mode,
1580 } => {
1581 let mut s = ov_f64(ov, id, "trim.start", start.value_at(frame)).clamp(0.0, 1.0);
1582 let mut e = ov_f64(ov, id, "trim.end", end.value_at(frame)).clamp(0.0, 1.0);
1583 if s > e {
1584 std::mem::swap(&mut s, &mut e);
1585 }
1586 let o = ov_f64(ov, id, "trim.offset", offset.value_at(frame)).rem_euclid(1.0);
1587
1588 if (e - s).abs() < 1e-9 {
1589 paths.clear();
1590 return;
1591 }
1592
1593 let originals = std::mem::take(paths);
1594 match mode {
1595 TrimMode::Individually => {
1596 for entry in originals {
1597 if let Some(trimmed) = trim_path(&entry.path, s, e, o) {
1598 paths.push(ShapeEntry {
1599 path: trimmed,
1600 ..entry
1601 });
1602 }
1603 }
1604 }
1605 TrimMode::Simultaneously => {
1606 let lengths: Vec<f64> = originals
1607 .iter()
1608 .map(|entry| entry.path.perimeter(1e-3))
1609 .collect();
1610 let total: f64 = lengths.iter().sum();
1611 if total <= 1e-9 {
1612 return;
1613 }
1614 let mut cursor = 0.0;
1615 for (entry, len) in originals.into_iter().zip(lengths) {
1616 let frac = len / total;
1617 if frac > 1e-12 {
1618 let ps = ((s - cursor) / frac).clamp(0.0, 1.0);
1619 let pe = ((e - cursor) / frac).clamp(0.0, 1.0);
1620 if pe > ps + 1e-9
1621 && let Some(t) = trim_path(&entry.path, ps, pe, o)
1622 {
1623 paths.push(ShapeEntry { path: t, ..entry });
1624 }
1625 }
1626 cursor += frac;
1627 }
1628 }
1629 }
1630 }
1631 ModifierKind::RoundCorners { radius } => {
1632 let r = ov_f64(ov, id, "round.radius", radius.value_at(frame)).max(0.0);
1633 if r > 1e-9 {
1634 for entry in paths.iter_mut() {
1641 let vp = renamite_geometry::VectorPath::from_bez_path(&entry.path);
1642 entry.path = vp.round_corners(r).to_bez_path();
1643 }
1644 }
1645 }
1646 ModifierKind::OffsetPath { amount } => {
1647 let amount = ov_f64(ov, id, "offset.amount", amount.value_at(frame));
1648 if amount.abs() > 1e-9 {
1649 for entry in paths.iter_mut() {
1650 if let Some(offset) = offset_bez_path(&entry.path, amount, SHAPE_TOL) {
1651 entry.path = offset;
1652 }
1653 }
1654 }
1655 }
1656 ModifierKind::ZigZag {
1657 amplitude,
1658 frequency,
1659 smooth,
1660 } => {
1661 let amp = ov_f64(ov, id, "zigzag.amplitude", amplitude.value_at(frame));
1662 let freq = ov_f64(ov, id, "zigzag.frequency", frequency.value_at(frame));
1663 if amp.abs() > 1e-9 && freq.abs() > 1e-9 {
1664 for entry in paths.iter_mut() {
1665 entry.path = renamite_geometry::zigzag_path(&entry.path, amp, freq, *smooth);
1666 }
1667 }
1668 }
1669 ModifierKind::PuckerBloat { amount } => {
1670 let amt = ov_f64(ov, id, "pucker.amount", amount.value_at(frame));
1671 if amt.abs() > 1e-9 {
1672 for entry in paths.iter_mut() {
1673 let vp = renamite_geometry::VectorPath::from_bez_path(&entry.path);
1674 entry.path =
1675 renamite_geometry::pucker_bloat_vector_path(&vp, amt).to_bez_path();
1676 }
1677 }
1678 }
1679 }
1680}
1681
1682fn trim_path(path: &BezPath, s: f64, e: f64, offset: f64) -> Option<BezPath> {
1683 use kurbo::ParamCurveArclen;
1684
1685 if (e - s).abs() < 1e-9 {
1686 return None;
1687 }
1688
1689 let segments: Vec<kurbo::PathSeg> = path.segments().collect();
1690 if segments.is_empty() {
1691 return None;
1692 }
1693 let lengths: Vec<f64> = segments.iter().map(|seg| seg.arclen(1e-3)).collect();
1694 let total: f64 = lengths.iter().sum();
1695 if total <= 1e-9 {
1696 return None;
1697 }
1698
1699 let s_offset = s + offset;
1700 let e_offset = e + offset;
1701 let wraps = s_offset < 1.0 && e_offset >= 1.0;
1705 let a = s_offset.rem_euclid(1.0);
1706 let b = e_offset.rem_euclid(1.0);
1707
1708 let mut out = BezPath::new();
1709 let mut last_end: Option<Point> = None;
1710 if wraps {
1711 emit_range(&segments, &lengths, total, a, 1.0, &mut out, &mut last_end);
1714 emit_range(&segments, &lengths, total, 0.0, b, &mut out, &mut last_end);
1715 } else {
1716 emit_range(&segments, &lengths, total, a, b, &mut out, &mut last_end);
1717 }
1718
1719 if out.elements().is_empty() {
1720 None
1721 } else {
1722 Some(out)
1723 }
1724}
1725
1726fn emit_range(
1727 segments: &[kurbo::PathSeg],
1728 lengths: &[f64],
1729 total: f64,
1730 a: f64,
1731 b: f64,
1732 out: &mut BezPath,
1733 last_end: &mut Option<Point>,
1734) {
1735 use kurbo::ParamCurve;
1736
1737 let a_len = a * total;
1738 let b_len = b * total;
1739 let mut cursor = 0.0;
1740
1741 for (seg, &len) in segments.iter().zip(lengths) {
1742 let seg_start = cursor;
1743 let seg_end = cursor + len;
1744 cursor = seg_end;
1745
1746 if seg_end <= a_len {
1747 continue;
1748 }
1749 if seg_start >= b_len {
1750 break;
1751 }
1752
1753 let t0 = if seg_start < a_len {
1754 arclen_to_t(seg, a_len - seg_start)
1755 } else {
1756 0.0
1757 };
1758 let t1 = if seg_end > b_len {
1759 arclen_to_t(seg, b_len - seg_start)
1760 } else {
1761 1.0
1762 };
1763 if t1 <= t0 + 1e-9 {
1764 continue;
1765 }
1766
1767 let sub = seg.subsegment(t0..t1);
1768 let start_pt = sub.start();
1769 let connected = last_end
1771 .map(|p| (p - start_pt).hypot() < 1e-6)
1772 .unwrap_or(false);
1773 if !connected {
1774 out.move_to(start_pt);
1775 }
1776 append_seg(out, &sub);
1777 *last_end = Some(sub.end());
1778 }
1779}
1780
1781fn arclen_to_t(seg: &kurbo::PathSeg, target: f64) -> f64 {
1782 use kurbo::{ParamCurve, ParamCurveArclen};
1783 let (mut lo, mut hi) = (0.0_f64, 1.0_f64);
1784 for _ in 0..24 {
1785 let mid = 0.5 * (lo + hi);
1786 if seg.subsegment(0.0..mid).arclen(1e-3) < target {
1787 lo = mid;
1788 } else {
1789 hi = mid;
1790 }
1791 }
1792 0.5 * (lo + hi)
1793}
1794
1795fn append_seg(out: &mut BezPath, seg: &kurbo::PathSeg) {
1796 match seg {
1797 kurbo::PathSeg::Line(l) => out.line_to(l.p1),
1798 kurbo::PathSeg::Quad(q) => out.quad_to(q.p1, q.p2),
1799 kurbo::PathSeg::Cubic(c) => out.curve_to(c.p1, c.p2, c.p3),
1800 }
1801}
1802
1803fn fold_gradient_point(affine: &Affine, local: glam::DVec2) -> glam::DVec2 {
1804 let p = *affine * Point::new(local.x, local.y);
1805 glam::DVec2::new(p.x, p.y)
1806}
1807
1808fn emit_style(
1809 st: &StyleKind,
1810 style_id: NodeId,
1811 frame: f64,
1812 ov: &Overrides,
1813 paths: &[ShapeEntry],
1814 opacity: f64,
1815 blend: BlendMode,
1816 active_clips: &[u32],
1817 scene: &mut Scene,
1818) {
1819 for e in paths {
1820 let (paint, kind, is_stroke) = match st {
1821 StyleKind::Fill { paint, rule } => (paint, PaintKind::Fill(*rule), false),
1822 StyleKind::Stroke {
1823 paint,
1824 width,
1825 cap,
1826 join,
1827 dash,
1828 } => (
1829 paint,
1830 PaintKind::Stroke(StrokeSample {
1831 width: ov_f64(ov, style_id, "stroke.width", width.value_at(frame)).max(0.0),
1832 cap: *cap,
1833 join: *join,
1834 dash: dash.as_ref().map(|d| DashSample {
1835 dashes: d.dashes.iter().map(|x| x.value_at(frame)).collect(),
1836 offset: d.offset.value_at(frame),
1837 }),
1838 }),
1839 true,
1840 ),
1841 };
1842
1843 let paint = sample_paint_world(paint, frame, &e.affine, ov, style_id, is_stroke);
1844
1845 scene.items.push(SceneItem {
1846 path: e.path.clone(),
1847 node: e.node,
1848 style: style_id,
1849 paint,
1850 kind,
1851 opacity: opacity * e.opacity,
1852 clips: active_clips.to_vec(),
1853 blend,
1854 });
1855 }
1856}
1857
1858fn sample_paint_world(
1859 paint: &StylePaint,
1860 frame: f64,
1861 affine: &Affine,
1862 ov: &Overrides,
1863 style_id: NodeId,
1864 is_stroke: bool,
1865) -> ScenePaint {
1866 match paint {
1867 StylePaint::Solid { color } => {
1868 let path = if is_stroke {
1869 "stroke.color"
1870 } else {
1871 "fill.color"
1872 };
1873 ScenePaint::Solid(ov_color(ov, style_id, path, color.value_at(frame)))
1874 }
1875 StylePaint::Gradient(g) => {
1876 let kind = g.kind;
1877 let start_local = ov_vec2(ov, style_id, "grad.start", g.start.value_at(frame));
1878 let end_local = ov_vec2(ov, style_id, "grad.end", g.end.value_at(frame));
1879 let stops = ov_stops(ov, style_id, "grad.stops", &g.stops.value_at(frame));
1880 match kind {
1881 GradientKind::Linear => ScenePaint::LinearGradient {
1882 start: fold_gradient_point(affine, start_local),
1883 end: fold_gradient_point(affine, end_local),
1884 stops,
1885 },
1886 GradientKind::Radial => ScenePaint::RadialGradient {
1887 center: fold_gradient_point(affine, start_local),
1888 end: fold_gradient_point(affine, end_local),
1889 stops,
1890 },
1891 }
1892 }
1893 }
1894}
1895
1896fn ov_stops(ov: &Overrides, id: NodeId, prop: &str, dflt: &GradientStops) -> GradientStops {
1897 match ov.get(id, prop) {
1898 Some(Value::Stops(s)) => s.clone(),
1899 _ => dflt.clone(),
1900 }
1901}
1902
1903#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1904pub enum Parent {
1905 Node(NodeId),
1906 Comp(CompId),
1907}
1908
1909#[derive(Clone, Debug, thiserror::Error)]
1910pub enum ModelError {
1911 #[error("node not found")]
1912 MissingNode,
1913 #[error("node kind mismatch (expected {0})")]
1914 WrongNodeKind(&'static str),
1915 #[error("composition not found")]
1916 MissingComp,
1917 #[error("no property at path {0}")]
1918 MissingProp(String),
1919 #[error("value type mismatch for {0}")]
1920 TypeMismatch(String),
1921 #[error("no keyframe at frame {0}")]
1922 NoKeyframe(i64),
1923 #[error("keyframe already exists at frame {0}")]
1924 KeyframeExists(i64),
1925 #[error("node is not attached")]
1926 NotAttached,
1927 #[error("asset not found")]
1928 MissingAsset,
1929}
1930
1931impl Document {
1932 pub fn empty() -> Self {
1933 let mut compositions = CompMap::default();
1934 let main = compositions.insert(Composition {
1935 name: "Main".into(),
1936 size: (512, 512),
1937 rate: renamite_animation::FrameRate { num: 60, den: 1 },
1938 range: (Frame(0), Frame(180)),
1939 children: Vec::new(),
1940 });
1941 Self {
1942 format_version: 1,
1943 compositions,
1944 nodes: NodeMap::default(),
1945 assets: AssetMap::default(),
1946 asset_order: Vec::new(),
1947 main,
1948 }
1949 }
1950
1951 pub fn create_node(&mut self, node: Node) -> NodeId {
1952 self.nodes.insert(node)
1953 }
1954
1955 pub fn attach(&mut self, id: NodeId, parent: Parent, index: usize) -> Result<(), ModelError> {
1956 if !self.nodes.contains_key(id) {
1957 return Err(ModelError::MissingNode);
1958 }
1959 match parent {
1960 Parent::Node(p) => {
1961 let pn = self.nodes.get_mut(p).ok_or(ModelError::MissingNode)?;
1962 let i = index.min(pn.children.len());
1963 pn.children.insert(i, id);
1964 self.nodes[id].parent = Some(p);
1965 }
1966 Parent::Comp(c) => {
1967 let comp = self
1968 .compositions
1969 .get_mut(c)
1970 .ok_or(ModelError::MissingComp)?;
1971 let i = index.min(comp.children.len());
1972 comp.children.insert(i, id);
1973 self.nodes[id].parent = None;
1974 }
1975 }
1976 Ok(())
1977 }
1978
1979 pub fn detach(&mut self, id: NodeId) -> Result<(Parent, usize), ModelError> {
1980 let (parent, index) = self.locate(id).ok_or(ModelError::NotAttached)?;
1981 match parent {
1982 Parent::Node(p) => {
1983 self.nodes[p].children.remove(index);
1984 }
1985 Parent::Comp(c) => {
1986 self.compositions[c].children.remove(index);
1987 }
1988 }
1989 if let Some(n) = self.nodes.get_mut(id) {
1990 n.parent = None;
1991 }
1992 Ok((parent, index))
1993 }
1994
1995 pub fn locate(&self, id: NodeId) -> Option<(Parent, usize)> {
1996 let n = self.nodes.get(id)?;
1997 if let Some(p) = n.parent {
1998 let i = self.nodes.get(p)?.children.iter().position(|&c| c == id)?;
1999 return Some((Parent::Node(p), i));
2000 }
2001 for (cid, comp) in &self.compositions {
2002 if let Some(i) = comp.children.iter().position(|&c| c == id) {
2003 return Some((Parent::Comp(cid), i));
2004 }
2005 }
2006 None
2007 }
2008
2009 pub fn garbage_collect(&mut self) {
2011 let mut live = std::collections::HashSet::new();
2012 fn mark(doc: &Document, id: NodeId, live: &mut std::collections::HashSet<NodeId>) {
2013 if !live.insert(id) {
2014 return;
2015 }
2016 if let Some(n) = doc.nodes.get(id) {
2017 for &c in &n.children {
2018 mark(doc, c, live);
2019 }
2020 }
2021 }
2022 let roots: Vec<NodeId> = self
2023 .compositions
2024 .values()
2025 .flat_map(|c| c.children.clone())
2026 .collect();
2027 for r in roots {
2028 mark(self, r, &mut live);
2029 }
2030 self.nodes.retain(|id, _| live.contains(&id));
2031
2032 let mut live_assets: std::collections::HashSet<AssetId> =
2034 self.asset_order.iter().copied().collect();
2035 for node in self.nodes.values() {
2036 if let NodeKind::Image(asset) = node.kind {
2037 live_assets.insert(asset);
2038 }
2039 }
2040 self.assets.retain(|id, _| live_assets.contains(&id));
2041 self.asset_order.retain(|id| self.assets.contains_key(*id));
2042 }
2043
2044 pub fn normalize_assets(&mut self) {
2048 let mut seen = std::collections::HashSet::new();
2049
2050 self.asset_order
2051 .retain(|id| self.assets.contains_key(*id) && seen.insert(*id));
2052
2053 for id in self.assets.keys() {
2054 if seen.insert(id) {
2055 self.asset_order.push(id);
2056 }
2057 }
2058 }
2059
2060 pub fn image_asset(&self, id: AssetId) -> Option<&ImageAsset> {
2062 match self.assets.get(id)? {
2063 Asset::Image(image) => Some(image),
2064 _ => None,
2065 }
2066 }
2067
2068 pub fn image_usage_count(&self, asset: AssetId) -> usize {
2070 self.nodes
2071 .values()
2072 .filter(|node| matches!(node.kind, NodeKind::Image(id) if id == asset))
2073 .count()
2074 }
2075
2076 pub fn font_asset_for_family(&self, family: &str) -> Option<(AssetId, &FontAsset)> {
2079 self.assets.iter().find_map(|(id, asset)| match asset {
2080 Asset::Font(font) if font.family == family => Some((id, font)),
2081 _ => None,
2082 })
2083 }
2084
2085 pub fn font_families(&self) -> Vec<String> {
2087 let mut out: Vec<String> = self
2088 .asset_order
2089 .iter()
2090 .filter_map(|id| match self.assets.get(*id) {
2091 Some(Asset::Font(font)) => Some(font.family.clone()),
2092 _ => None,
2093 })
2094 .collect();
2095 out.sort();
2096 out.dedup();
2097 out
2098 }
2099}
2100
2101#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
2102pub struct PropPath(pub String);
2103
2104impl PropPath {
2105 pub fn new(s: impl Into<String>) -> Self {
2106 Self(s.into())
2107 }
2108 pub fn as_str(&self) -> &str {
2109 &self.0
2110 }
2111}
2112
2113#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2114pub enum Value {
2115 F64(f64),
2116 DVec2(glam::DVec2),
2117 Angle(Angle),
2118 Color(Color),
2119 Path(VectorPath),
2120 Bool(bool),
2121 I64(i64),
2122 Stops(GradientStops),
2124 Paint(StylePaint),
2126}
2127
2128pub fn pick(scene: &Scene, pt: glam::DVec2) -> Option<NodeId> {
2130 let q = Point::new(pt.x, pt.y);
2131 for item in scene.items.iter().rev() {
2132 if item.opacity <= 0.0 {
2133 continue;
2134 }
2135
2136 let dashed_path = match &item.kind {
2137 PaintKind::Stroke(stroke) => stroke
2138 .dash
2139 .as_ref()
2140 .and_then(|dash| dash_bez_path(&item.path, &dash.dashes, dash.offset)),
2141 PaintKind::Fill(_) => None,
2142 };
2143
2144 let hit_path = dashed_path.as_ref().unwrap_or(&item.path);
2145
2146 let padding = match &item.kind {
2147 PaintKind::Stroke(stroke) => (stroke.width * 0.5).max(1.0),
2148 PaintKind::Fill(_) => 0.0,
2149 };
2150
2151 if !hit_path
2152 .bounding_box()
2153 .inflate(padding, padding)
2154 .contains(q)
2155 {
2156 continue;
2157 }
2158 let clips_ok = item.clips.iter().all(|&ci| {
2159 let Some(c) = scene.clips.get(ci as usize) else {
2160 return false; };
2162 match c.rule {
2163 FillRule::NonZero => c.path.winding(q) != 0,
2164 FillRule::EvenOdd => c.path.winding(q) % 2 != 0,
2165 }
2166 });
2167 if !clips_ok {
2168 continue;
2169 }
2170 let hit = match &item.kind {
2171 PaintKind::Fill(rule) => match rule {
2172 FillRule::NonZero => hit_path.winding(q) != 0,
2173 FillRule::EvenOdd => hit_path.winding(q) % 2 != 0,
2174 },
2175 PaintKind::Stroke(_) => nearest_dist(hit_path, q) <= padding,
2176 };
2177 if hit {
2178 return Some(item.node);
2179 }
2180 }
2181 None
2182}
2183
2184fn nearest_dist(path: &BezPath, q: Point) -> f64 {
2185 let mut best = f64::MAX;
2186 for seg in path.segments() {
2187 best = best.min(seg.nearest(q, 1e-6).distance_sq);
2188 }
2189 best.sqrt()
2190}
2191
2192pub fn pick_box(scene: &Scene, min: glam::DVec2, max: glam::DVec2) -> Vec<NodeId> {
2194 let mut out = Vec::new();
2195 for item in &scene.items {
2196 if item.opacity <= 0.0 {
2197 continue;
2198 }
2199 let bb = item.path.bounding_box();
2200 if bb.x0 >= min.x
2201 && bb.x1 <= max.x
2202 && bb.y0 >= min.y
2203 && bb.y1 <= max.y
2204 && !out.contains(&item.node)
2205 {
2206 out.push(item.node);
2207 }
2208 }
2209 out
2210}
2211
2212pub fn nodes_bounds(scene: &Scene, nodes: &[NodeId]) -> Option<(glam::DVec2, glam::DVec2)> {
2214 let mut acc: Option<kurbo::Rect> = None;
2215 for item in &scene.items {
2216 if !nodes.contains(&item.node) {
2217 continue;
2218 }
2219 let bb = item.path.bounding_box();
2220 acc = Some(acc.map_or(bb, |a| a.union(bb)));
2221 }
2222 acc.map(|r| (glam::DVec2::new(r.x0, r.y0), glam::DVec2::new(r.x1, r.y1)))
2223}
2224
2225fn transform_vector(affine: Affine, value: glam::DVec2) -> glam::DVec2 {
2226 let [a, b, c, d, _, _] = affine.as_coeffs();
2227
2228 glam::DVec2::new(a * value.x + c * value.y, b * value.x + d * value.y)
2229}
2230
2231pub fn world_delta_to_parent(
2233 doc: &Document,
2234 id: NodeId,
2235 frame: f64,
2236 delta: glam::DVec2,
2237) -> Option<glam::DVec2> {
2238 let context = node_transform_context(doc, id, frame)?;
2239 let inverse = context.parent_world.inverse();
2240
2241 let result = transform_vector(inverse, delta);
2242
2243 result.is_finite().then_some(result)
2244}
2245
2246pub fn node_is_ancestor(doc: &Document, ancestor: NodeId, mut node: NodeId) -> bool {
2247 while let Some(current) = doc.nodes.get(node) {
2248 let Some(parent) = current.parent else {
2249 return false;
2250 };
2251
2252 if parent == ancestor {
2253 return true;
2254 }
2255
2256 node = parent;
2257 }
2258
2259 false
2260}
2261
2262pub fn selected_ancestor_for_pick(
2265 doc: &Document,
2266 picked: NodeId,
2267 selection: &[NodeId],
2268) -> Option<NodeId> {
2269 selection
2270 .iter()
2271 .copied()
2272 .find(|selected| *selected == picked || node_is_ancestor(doc, *selected, picked))
2273}
2274
2275pub fn immediate_child_below(
2277 doc: &Document,
2278 ancestor: NodeId,
2279 descendant: NodeId,
2280) -> Option<NodeId> {
2281 if ancestor == descendant {
2282 return None;
2283 }
2284
2285 let mut current = descendant;
2286
2287 loop {
2288 let parent = doc.nodes.get(current)?.parent?;
2289
2290 if parent == ancestor {
2291 return Some(current);
2292 }
2293
2294 current = parent;
2295 }
2296}
2297
2298pub fn selection_bounds(
2301 doc: &Document,
2302 scene: &Scene,
2303 selection: &[NodeId],
2304) -> Option<(glam::DVec2, glam::DVec2)> {
2305 let mut bounds: Option<kurbo::Rect> = None;
2306
2307 for item in &scene.items {
2308 let included = selection
2309 .iter()
2310 .copied()
2311 .any(|selected| selected == item.node || node_is_ancestor(doc, selected, item.node));
2312
2313 if !included {
2314 continue;
2315 }
2316
2317 let item_bounds = item.path.bounding_box();
2318
2319 bounds = Some(match bounds {
2320 Some(existing) => existing.union(item_bounds),
2321 None => item_bounds,
2322 });
2323 }
2324
2325 bounds.map(|rect| {
2326 (
2327 glam::DVec2::new(rect.x0, rect.y0),
2328 glam::DVec2::new(rect.x1, rect.y1),
2329 )
2330 })
2331}
2332
2333#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2335pub struct KeyframeData {
2336 pub frame: Frame,
2337 pub value: Value,
2338 pub interpolation: Interpolation,
2339 pub ease_out: EasingHandle,
2340 pub ease_in: EasingHandle,
2341}
2342
2343pub trait PropValue: Tween + Clone {
2344 fn into_value(self) -> Value;
2345 fn from_value(v: &Value) -> Option<Self>;
2346}
2347impl PropValue for f64 {
2348 fn into_value(self) -> Value {
2349 Value::F64(self)
2350 }
2351 fn from_value(v: &Value) -> Option<Self> {
2352 if let Value::F64(x) = v {
2353 Some(*x)
2354 } else {
2355 None
2356 }
2357 }
2358}
2359impl PropValue for glam::DVec2 {
2360 fn into_value(self) -> Value {
2361 Value::DVec2(self)
2362 }
2363 fn from_value(v: &Value) -> Option<Self> {
2364 if let Value::DVec2(x) = v {
2365 Some(*x)
2366 } else {
2367 None
2368 }
2369 }
2370}
2371impl PropValue for Angle {
2372 fn into_value(self) -> Value {
2373 Value::Angle(self)
2374 }
2375 fn from_value(v: &Value) -> Option<Self> {
2376 match v {
2377 Value::Angle(a) => Some(*a),
2378 Value::F64(x) => Some(Angle(*x)),
2379 _ => None,
2380 }
2381 }
2382}
2383impl PropValue for Color {
2384 fn into_value(self) -> Value {
2385 Value::Color(self)
2386 }
2387 fn from_value(v: &Value) -> Option<Self> {
2388 if let Value::Color(c) = v {
2389 Some(*c)
2390 } else {
2391 None
2392 }
2393 }
2394}
2395impl PropValue for VectorPath {
2396 fn into_value(self) -> Value {
2397 Value::Path(self)
2398 }
2399 fn from_value(v: &Value) -> Option<Self> {
2400 if let Value::Path(p) = v {
2401 Some(p.clone())
2402 } else {
2403 None
2404 }
2405 }
2406}
2407impl PropValue for GradientStops {
2408 fn into_value(self) -> Value {
2409 Value::Stops(self)
2410 }
2411 fn from_value(v: &Value) -> Option<Self> {
2412 if let Value::Stops(s) = v {
2413 Some(s.clone())
2414 } else {
2415 None
2416 }
2417 }
2418}
2419impl PropValue for StylePaint {
2420 fn into_value(self) -> Value {
2421 Value::Paint(self)
2422 }
2423 fn from_value(v: &Value) -> Option<Self> {
2424 if let Value::Paint(p) = v {
2425 Some(p.clone())
2426 } else {
2427 None
2428 }
2429 }
2430}
2431
2432pub enum PropMut<'a> {
2433 F64(&'a mut Animated<f64>),
2434 Vec2(&'a mut Animated<glam::DVec2>),
2435 Angle(&'a mut Animated<Angle>),
2436 Color(&'a mut Animated<Color>),
2437 Path(&'a mut Animated<VectorPath>),
2438 Stops(&'a mut Animated<GradientStops>),
2439}
2440pub enum PropRef<'a> {
2441 F64(&'a Animated<f64>),
2442 Vec2(&'a Animated<glam::DVec2>),
2443 Angle(&'a Animated<Angle>),
2444 Color(&'a Animated<Color>),
2445 Path(&'a Animated<VectorPath>),
2446 Stops(&'a Animated<GradientStops>),
2447}
2448
2449pub trait PropVisitor {
2450 type Out;
2451 fn visit<T: PropValue>(self, a: &mut Animated<T>) -> Self::Out;
2452}
2453pub trait PropReader {
2454 type Out;
2455 fn read<T: PropValue>(self, a: &Animated<T>) -> Self::Out;
2456}
2457
2458pub fn visit_prop<V: PropVisitor>(p: PropMut<'_>, v: V) -> V::Out {
2459 match p {
2460 PropMut::F64(a) => v.visit(a),
2461 PropMut::Vec2(a) => v.visit(a),
2462 PropMut::Angle(a) => v.visit(a),
2463 PropMut::Color(a) => v.visit(a),
2464 PropMut::Path(a) => v.visit(a),
2465 PropMut::Stops(a) => v.visit(a),
2466 }
2467}
2468pub fn read_prop<V: PropReader>(p: PropRef<'_>, v: V) -> V::Out {
2469 match p {
2470 PropRef::F64(a) => v.read(a),
2471 PropRef::Vec2(a) => v.read(a),
2472 PropRef::Angle(a) => v.read(a),
2473 PropRef::Color(a) => v.read(a),
2474 PropRef::Path(a) => v.read(a),
2475 PropRef::Stops(a) => v.read(a),
2476 }
2477}
2478
2479fn dash_index(path: &str) -> Option<usize> {
2480 path.strip_prefix("stroke.dash.")?.parse().ok()
2481}
2482
2483impl Node {
2484 pub fn prop_mut(&mut self, prop: &PropPath) -> Option<PropMut<'_>> {
2485 use PropMut::*;
2486 let s = prop.as_str();
2487 if s == "stroke.dash.offset" || dash_index(s).is_some() {
2488 if let NodeKind::Style(StyleKind::Stroke {
2489 dash: Some(dash), ..
2490 }) = &mut self.kind
2491 {
2492 if s == "stroke.dash.offset" {
2493 return Some(F64(&mut dash.offset));
2494 }
2495 if let Some(index) = dash_index(s) {
2496 return dash.dashes.get_mut(index).map(PropMut::F64);
2497 }
2498 }
2499 return None;
2500 }
2501
2502 match (s, &mut self.kind) {
2503 ("opacity", _) => Some(F64(&mut self.opacity)),
2504 ("transform.anchor", _) => Some(Vec2(&mut self.transform.anchor)),
2505 ("transform.position", _) => Some(Vec2(&mut self.transform.position)),
2506 ("transform.scale", _) => Some(Vec2(&mut self.transform.scale)),
2507 ("transform.rotation", _) => Some(Angle(&mut self.transform.rotation)),
2508 ("transform.skew", _) => Some(F64(&mut self.transform.skew)),
2509 ("transform.skew_axis", _) => Some(F64(&mut self.transform.skew_axis)),
2510 ("shape.path", NodeKind::Shape(ShapeKind::Path(p))) => Some(Path(p)),
2511 ("shape.pos", NodeKind::Shape(ShapeKind::Rect { pos, .. }))
2512 | ("shape.pos", NodeKind::Shape(ShapeKind::Ellipse { pos, .. }))
2513 | ("shape.pos", NodeKind::Shape(ShapeKind::Star { pos, .. }))
2514 | ("shape.pos", NodeKind::Shape(ShapeKind::Polygon { pos, .. })) => Some(Vec2(pos)),
2515 ("shape.size", NodeKind::Shape(ShapeKind::Rect { size, .. }))
2516 | ("shape.size", NodeKind::Shape(ShapeKind::Ellipse { size, .. })) => Some(Vec2(size)),
2517 ("shape.rounded", NodeKind::Shape(ShapeKind::Rect { rounded, .. })) => {
2518 Some(F64(rounded))
2519 }
2520 ("shape.points", NodeKind::Shape(ShapeKind::Star { points, .. }))
2521 | ("shape.points", NodeKind::Shape(ShapeKind::Polygon { points, .. })) => {
2522 Some(F64(points))
2523 }
2524 ("shape.inner_r", NodeKind::Shape(ShapeKind::Star { inner_r, .. })) => {
2525 Some(F64(inner_r))
2526 }
2527 ("shape.outer_r", NodeKind::Shape(ShapeKind::Star { outer_r, .. }))
2528 | ("shape.outer_r", NodeKind::Shape(ShapeKind::Polygon { outer_r, .. })) => {
2529 Some(F64(outer_r))
2530 }
2531 ("shape.roundness", NodeKind::Shape(ShapeKind::Star { roundness, .. }))
2532 | ("shape.roundness", NodeKind::Shape(ShapeKind::Polygon { roundness, .. })) => {
2533 Some(F64(roundness))
2534 }
2535 ("text.size", NodeKind::Text(t)) => Some(F64(&mut t.size)),
2536 (
2537 "shape.path",
2538 NodeKind::Mask(MaskProps {
2539 shape: ShapeKind::Path(p),
2540 ..
2541 }),
2542 ) => Some(Path(p)),
2543 (
2544 "shape.pos",
2545 NodeKind::Mask(MaskProps {
2546 shape: ShapeKind::Rect { pos, .. },
2547 ..
2548 }),
2549 )
2550 | (
2551 "shape.pos",
2552 NodeKind::Mask(MaskProps {
2553 shape: ShapeKind::Ellipse { pos, .. },
2554 ..
2555 }),
2556 )
2557 | (
2558 "shape.pos",
2559 NodeKind::Mask(MaskProps {
2560 shape: ShapeKind::Star { pos, .. },
2561 ..
2562 }),
2563 )
2564 | (
2565 "shape.pos",
2566 NodeKind::Mask(MaskProps {
2567 shape: ShapeKind::Polygon { pos, .. },
2568 ..
2569 }),
2570 ) => Some(Vec2(pos)),
2571 (
2572 "shape.size",
2573 NodeKind::Mask(MaskProps {
2574 shape: ShapeKind::Rect { size, .. },
2575 ..
2576 }),
2577 )
2578 | (
2579 "shape.size",
2580 NodeKind::Mask(MaskProps {
2581 shape: ShapeKind::Ellipse { size, .. },
2582 ..
2583 }),
2584 ) => Some(Vec2(size)),
2585 (
2586 "shape.rounded",
2587 NodeKind::Mask(MaskProps {
2588 shape: ShapeKind::Rect { rounded, .. },
2589 ..
2590 }),
2591 ) => Some(F64(rounded)),
2592 (
2593 "shape.points",
2594 NodeKind::Mask(MaskProps {
2595 shape: ShapeKind::Star { points, .. } | ShapeKind::Polygon { points, .. },
2596 ..
2597 }),
2598 ) => Some(F64(points)),
2599 (
2600 "shape.inner_r",
2601 NodeKind::Mask(MaskProps {
2602 shape: ShapeKind::Star { inner_r, .. },
2603 ..
2604 }),
2605 ) => Some(F64(inner_r)),
2606 (
2607 "shape.outer_r",
2608 NodeKind::Mask(MaskProps {
2609 shape: ShapeKind::Star { outer_r, .. } | ShapeKind::Polygon { outer_r, .. },
2610 ..
2611 }),
2612 ) => Some(F64(outer_r)),
2613 (
2614 "shape.roundness",
2615 NodeKind::Mask(MaskProps {
2616 shape: ShapeKind::Star { roundness, .. } | ShapeKind::Polygon { roundness, .. },
2617 ..
2618 }),
2619 ) => Some(F64(roundness)),
2620 (
2621 "fill.color",
2622 NodeKind::Style(StyleKind::Fill {
2623 paint: StylePaint::Solid { color },
2624 ..
2625 }),
2626 ) => Some(Color(color)),
2627 (
2628 "stroke.color",
2629 NodeKind::Style(StyleKind::Stroke {
2630 paint: StylePaint::Solid { color },
2631 ..
2632 }),
2633 ) => Some(Color(color)),
2634 ("stroke.width", NodeKind::Style(StyleKind::Stroke { width, .. })) => Some(F64(width)),
2635 (
2636 "grad.start",
2637 NodeKind::Style(StyleKind::Fill {
2638 paint: StylePaint::Gradient(g),
2639 ..
2640 }),
2641 )
2642 | (
2643 "grad.start",
2644 NodeKind::Style(StyleKind::Stroke {
2645 paint: StylePaint::Gradient(g),
2646 ..
2647 }),
2648 ) => Some(Vec2(&mut g.start)),
2649 (
2650 "grad.end",
2651 NodeKind::Style(StyleKind::Fill {
2652 paint: StylePaint::Gradient(g),
2653 ..
2654 }),
2655 )
2656 | (
2657 "grad.end",
2658 NodeKind::Style(StyleKind::Stroke {
2659 paint: StylePaint::Gradient(g),
2660 ..
2661 }),
2662 ) => Some(Vec2(&mut g.end)),
2663 (
2664 "grad.stops",
2665 NodeKind::Style(StyleKind::Fill {
2666 paint: StylePaint::Gradient(g),
2667 ..
2668 }),
2669 )
2670 | (
2671 "grad.stops",
2672 NodeKind::Style(StyleKind::Stroke {
2673 paint: StylePaint::Gradient(g),
2674 ..
2675 }),
2676 ) => Some(Stops(&mut g.stops)),
2677 ("trim.start", NodeKind::Modifier(ModifierKind::TrimPath { start, .. })) => {
2678 Some(F64(start))
2679 }
2680 ("trim.end", NodeKind::Modifier(ModifierKind::TrimPath { end, .. })) => Some(F64(end)),
2681 ("trim.offset", NodeKind::Modifier(ModifierKind::TrimPath { offset, .. })) => {
2682 Some(F64(offset))
2683 }
2684 ("repeater.copies", NodeKind::Modifier(ModifierKind::Repeater { copies, .. })) => {
2685 Some(F64(copies))
2686 }
2687 ("repeater.offset", NodeKind::Modifier(ModifierKind::Repeater { offset, .. })) => {
2688 Some(F64(offset))
2689 }
2690 (
2691 "repeater.start_opacity",
2692 NodeKind::Modifier(ModifierKind::Repeater { start_opacity, .. }),
2693 ) => Some(F64(start_opacity)),
2694 (
2695 "repeater.end_opacity",
2696 NodeKind::Modifier(ModifierKind::Repeater { end_opacity, .. }),
2697 ) => Some(F64(end_opacity)),
2698 (
2699 "repeater.transform.position",
2700 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2701 ) => Some(Vec2(&mut transform.position)),
2702 (
2703 "repeater.transform.scale",
2704 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2705 ) => Some(Vec2(&mut transform.scale)),
2706 (
2707 "repeater.transform.rotation",
2708 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2709 ) => Some(Angle(&mut transform.rotation)),
2710 (
2711 "repeater.transform.anchor",
2712 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2713 ) => Some(Vec2(&mut transform.anchor)),
2714 (
2715 "repeater.transform.skew",
2716 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2717 ) => Some(F64(&mut transform.skew)),
2718 (
2719 "repeater.transform.skew_axis",
2720 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2721 ) => Some(F64(&mut transform.skew_axis)),
2722 ("round.radius", NodeKind::Modifier(ModifierKind::RoundCorners { radius })) => {
2723 Some(F64(radius))
2724 }
2725 ("offset.amount", NodeKind::Modifier(ModifierKind::OffsetPath { amount })) => {
2726 Some(F64(amount))
2727 }
2728 ("zigzag.amplitude", NodeKind::Modifier(ModifierKind::ZigZag { amplitude, .. })) => {
2729 Some(F64(amplitude))
2730 }
2731 ("zigzag.frequency", NodeKind::Modifier(ModifierKind::ZigZag { frequency, .. })) => {
2732 Some(F64(frequency))
2733 }
2734 ("pucker.amount", NodeKind::Modifier(ModifierKind::PuckerBloat { amount })) => {
2735 Some(F64(amount))
2736 }
2737 _ => None,
2738 }
2739 }
2740
2741 pub fn prop_ref(&self, prop: &PropPath) -> Option<PropRef<'_>> {
2742 use PropRef::*;
2743 let s = prop.as_str();
2744 if s == "stroke.dash.offset" || dash_index(s).is_some() {
2745 if let NodeKind::Style(StyleKind::Stroke {
2746 dash: Some(dash), ..
2747 }) = &self.kind
2748 {
2749 if s == "stroke.dash.offset" {
2750 return Some(F64(&dash.offset));
2751 }
2752 if let Some(index) = dash_index(s) {
2753 return dash.dashes.get(index).map(PropRef::F64);
2754 }
2755 }
2756 return None;
2757 }
2758
2759 match (s, &self.kind) {
2760 ("opacity", _) => Some(F64(&self.opacity)),
2761 ("transform.anchor", _) => Some(Vec2(&self.transform.anchor)),
2762 ("transform.position", _) => Some(Vec2(&self.transform.position)),
2763 ("transform.scale", _) => Some(Vec2(&self.transform.scale)),
2764 ("transform.rotation", _) => Some(Angle(&self.transform.rotation)),
2765 ("transform.skew", _) => Some(F64(&self.transform.skew)),
2766 ("transform.skew_axis", _) => Some(F64(&self.transform.skew_axis)),
2767 ("shape.path", NodeKind::Shape(ShapeKind::Path(p))) => Some(Path(p)),
2768 ("shape.pos", NodeKind::Shape(ShapeKind::Rect { pos, .. }))
2769 | ("shape.pos", NodeKind::Shape(ShapeKind::Ellipse { pos, .. }))
2770 | ("shape.pos", NodeKind::Shape(ShapeKind::Star { pos, .. }))
2771 | ("shape.pos", NodeKind::Shape(ShapeKind::Polygon { pos, .. })) => Some(Vec2(pos)),
2772 ("shape.size", NodeKind::Shape(ShapeKind::Rect { size, .. }))
2773 | ("shape.size", NodeKind::Shape(ShapeKind::Ellipse { size, .. })) => Some(Vec2(size)),
2774 ("shape.rounded", NodeKind::Shape(ShapeKind::Rect { rounded, .. })) => {
2775 Some(F64(rounded))
2776 }
2777 ("shape.points", NodeKind::Shape(ShapeKind::Star { points, .. }))
2778 | ("shape.points", NodeKind::Shape(ShapeKind::Polygon { points, .. })) => {
2779 Some(F64(points))
2780 }
2781 ("shape.inner_r", NodeKind::Shape(ShapeKind::Star { inner_r, .. })) => {
2782 Some(F64(inner_r))
2783 }
2784 ("shape.outer_r", NodeKind::Shape(ShapeKind::Star { outer_r, .. }))
2785 | ("shape.outer_r", NodeKind::Shape(ShapeKind::Polygon { outer_r, .. })) => {
2786 Some(F64(outer_r))
2787 }
2788 ("shape.roundness", NodeKind::Shape(ShapeKind::Star { roundness, .. }))
2789 | ("shape.roundness", NodeKind::Shape(ShapeKind::Polygon { roundness, .. })) => {
2790 Some(F64(roundness))
2791 }
2792 ("text.size", NodeKind::Text(t)) => Some(F64(&t.size)),
2793 (
2794 "shape.path",
2795 NodeKind::Mask(MaskProps {
2796 shape: ShapeKind::Path(p),
2797 ..
2798 }),
2799 ) => Some(Path(p)),
2800 (
2801 "shape.pos",
2802 NodeKind::Mask(MaskProps {
2803 shape: ShapeKind::Rect { pos, .. },
2804 ..
2805 }),
2806 )
2807 | (
2808 "shape.pos",
2809 NodeKind::Mask(MaskProps {
2810 shape: ShapeKind::Ellipse { pos, .. },
2811 ..
2812 }),
2813 )
2814 | (
2815 "shape.pos",
2816 NodeKind::Mask(MaskProps {
2817 shape: ShapeKind::Star { pos, .. },
2818 ..
2819 }),
2820 )
2821 | (
2822 "shape.pos",
2823 NodeKind::Mask(MaskProps {
2824 shape: ShapeKind::Polygon { pos, .. },
2825 ..
2826 }),
2827 ) => Some(Vec2(pos)),
2828 (
2829 "shape.size",
2830 NodeKind::Mask(MaskProps {
2831 shape: ShapeKind::Rect { size, .. },
2832 ..
2833 }),
2834 )
2835 | (
2836 "shape.size",
2837 NodeKind::Mask(MaskProps {
2838 shape: ShapeKind::Ellipse { size, .. },
2839 ..
2840 }),
2841 ) => Some(Vec2(size)),
2842 (
2843 "shape.rounded",
2844 NodeKind::Mask(MaskProps {
2845 shape: ShapeKind::Rect { rounded, .. },
2846 ..
2847 }),
2848 ) => Some(F64(rounded)),
2849 (
2850 "shape.points",
2851 NodeKind::Mask(MaskProps {
2852 shape: ShapeKind::Star { points, .. } | ShapeKind::Polygon { points, .. },
2853 ..
2854 }),
2855 ) => Some(F64(points)),
2856 (
2857 "shape.inner_r",
2858 NodeKind::Mask(MaskProps {
2859 shape: ShapeKind::Star { inner_r, .. },
2860 ..
2861 }),
2862 ) => Some(F64(inner_r)),
2863 (
2864 "shape.outer_r",
2865 NodeKind::Mask(MaskProps {
2866 shape: ShapeKind::Star { outer_r, .. } | ShapeKind::Polygon { outer_r, .. },
2867 ..
2868 }),
2869 ) => Some(F64(outer_r)),
2870 (
2871 "shape.roundness",
2872 NodeKind::Mask(MaskProps {
2873 shape: ShapeKind::Star { roundness, .. } | ShapeKind::Polygon { roundness, .. },
2874 ..
2875 }),
2876 ) => Some(F64(roundness)),
2877 (
2878 "fill.color",
2879 NodeKind::Style(StyleKind::Fill {
2880 paint: StylePaint::Solid { color },
2881 ..
2882 }),
2883 ) => Some(Color(color)),
2884 (
2885 "stroke.color",
2886 NodeKind::Style(StyleKind::Stroke {
2887 paint: StylePaint::Solid { color },
2888 ..
2889 }),
2890 ) => Some(Color(color)),
2891 ("stroke.width", NodeKind::Style(StyleKind::Stroke { width, .. })) => Some(F64(width)),
2892 (
2893 "grad.start",
2894 NodeKind::Style(StyleKind::Fill {
2895 paint: StylePaint::Gradient(g),
2896 ..
2897 }),
2898 )
2899 | (
2900 "grad.start",
2901 NodeKind::Style(StyleKind::Stroke {
2902 paint: StylePaint::Gradient(g),
2903 ..
2904 }),
2905 ) => Some(Vec2(&g.start)),
2906 (
2907 "grad.end",
2908 NodeKind::Style(StyleKind::Fill {
2909 paint: StylePaint::Gradient(g),
2910 ..
2911 }),
2912 )
2913 | (
2914 "grad.end",
2915 NodeKind::Style(StyleKind::Stroke {
2916 paint: StylePaint::Gradient(g),
2917 ..
2918 }),
2919 ) => Some(Vec2(&g.end)),
2920 (
2921 "grad.stops",
2922 NodeKind::Style(StyleKind::Fill {
2923 paint: StylePaint::Gradient(g),
2924 ..
2925 }),
2926 )
2927 | (
2928 "grad.stops",
2929 NodeKind::Style(StyleKind::Stroke {
2930 paint: StylePaint::Gradient(g),
2931 ..
2932 }),
2933 ) => Some(Stops(&g.stops)),
2934 ("trim.start", NodeKind::Modifier(ModifierKind::TrimPath { start, .. })) => {
2935 Some(F64(start))
2936 }
2937 ("trim.end", NodeKind::Modifier(ModifierKind::TrimPath { end, .. })) => Some(F64(end)),
2938 ("trim.offset", NodeKind::Modifier(ModifierKind::TrimPath { offset, .. })) => {
2939 Some(F64(offset))
2940 }
2941 ("repeater.copies", NodeKind::Modifier(ModifierKind::Repeater { copies, .. })) => {
2942 Some(F64(copies))
2943 }
2944 ("repeater.offset", NodeKind::Modifier(ModifierKind::Repeater { offset, .. })) => {
2945 Some(F64(offset))
2946 }
2947 (
2948 "repeater.start_opacity",
2949 NodeKind::Modifier(ModifierKind::Repeater { start_opacity, .. }),
2950 ) => Some(F64(start_opacity)),
2951 (
2952 "repeater.end_opacity",
2953 NodeKind::Modifier(ModifierKind::Repeater { end_opacity, .. }),
2954 ) => Some(F64(end_opacity)),
2955 (
2956 "repeater.transform.position",
2957 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2958 ) => Some(Vec2(&transform.position)),
2959 (
2960 "repeater.transform.scale",
2961 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2962 ) => Some(Vec2(&transform.scale)),
2963 (
2964 "repeater.transform.rotation",
2965 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2966 ) => Some(Angle(&transform.rotation)),
2967 (
2968 "repeater.transform.anchor",
2969 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2970 ) => Some(Vec2(&transform.anchor)),
2971 (
2972 "repeater.transform.skew",
2973 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2974 ) => Some(F64(&transform.skew)),
2975 (
2976 "repeater.transform.skew_axis",
2977 NodeKind::Modifier(ModifierKind::Repeater { transform, .. }),
2978 ) => Some(F64(&transform.skew_axis)),
2979 ("round.radius", NodeKind::Modifier(ModifierKind::RoundCorners { radius })) => {
2980 Some(F64(radius))
2981 }
2982 ("offset.amount", NodeKind::Modifier(ModifierKind::OffsetPath { amount })) => {
2983 Some(F64(amount))
2984 }
2985 ("zigzag.amplitude", NodeKind::Modifier(ModifierKind::ZigZag { amplitude, .. })) => {
2986 Some(F64(amplitude))
2987 }
2988 ("zigzag.frequency", NodeKind::Modifier(ModifierKind::ZigZag { frequency, .. })) => {
2989 Some(F64(frequency))
2990 }
2991 ("pucker.amount", NodeKind::Modifier(ModifierKind::PuckerBloat { amount })) => {
2992 Some(F64(amount))
2993 }
2994 _ => None,
2995 }
2996 }
2997}
2998
2999struct SetStaticOp<'a>(&'a Value, &'a str);
3000impl PropVisitor for SetStaticOp<'_> {
3001 type Out = Result<Value, ModelError>;
3002 fn visit<T: PropValue>(self, a: &mut Animated<T>) -> Self::Out {
3003 let new = T::from_value(self.0).ok_or_else(|| ModelError::TypeMismatch(self.1.into()))?;
3004 Ok(std::mem::replace(&mut a.base, new).into_value())
3005 }
3006}
3007
3008struct AddKeyOp<'a> {
3009 frame: Frame,
3010 value: &'a Value,
3011 prop: &'a str,
3012}
3013impl PropVisitor for AddKeyOp<'_> {
3014 type Out = Result<Option<KeyframeData>, ModelError>;
3015 fn visit<T: PropValue>(self, a: &mut Animated<T>) -> Self::Out {
3016 let new =
3017 T::from_value(self.value).ok_or_else(|| ModelError::TypeMismatch(self.prop.into()))?;
3018 let old = a.key_at(self.frame).map(|k| KeyframeData {
3019 frame: k.frame,
3020 value: k.value.clone().into_value(),
3021 interpolation: k.interpolation,
3022 ease_out: k.ease_out,
3023 ease_in: k.ease_in,
3024 });
3025 a.set_key(self.frame, new);
3026 Ok(old)
3027 }
3028}
3029
3030struct RemoveKeyOp(Frame);
3031impl PropVisitor for RemoveKeyOp {
3032 type Out = Result<KeyframeData, ModelError>;
3033 fn visit<T: PropValue>(self, a: &mut Animated<T>) -> Self::Out {
3034 let k = a
3035 .remove_key(self.0)
3036 .ok_or(ModelError::NoKeyframe(self.0.0))?;
3037 Ok(KeyframeData {
3038 frame: k.frame,
3039 value: k.value.into_value(),
3040 interpolation: k.interpolation,
3041 ease_out: k.ease_out,
3042 ease_in: k.ease_in,
3043 })
3044 }
3045}
3046
3047struct RestoreKeyOp<'a>(&'a KeyframeData, &'a str);
3048impl PropVisitor for RestoreKeyOp<'_> {
3049 type Out = Result<(), ModelError>;
3050 fn visit<T: PropValue>(self, a: &mut Animated<T>) -> Self::Out {
3051 let v =
3052 T::from_value(&self.0.value).ok_or_else(|| ModelError::TypeMismatch(self.1.into()))?;
3053 a.set_key(self.0.frame, v);
3054 a.set_easing(
3055 self.0.frame,
3056 self.0.interpolation,
3057 self.0.ease_out,
3058 self.0.ease_in,
3059 );
3060 Ok(())
3061 }
3062}
3063
3064struct MoveKeyOp {
3065 from: Frame,
3066 to: Frame,
3067}
3068impl PropVisitor for MoveKeyOp {
3069 type Out = Result<(), ModelError>;
3070 fn visit<T: PropValue>(self, a: &mut Animated<T>) -> Self::Out {
3071 if self.from == self.to {
3072 return Ok(());
3073 }
3074 if a.key_at(self.to).is_some() {
3075 return Err(ModelError::KeyframeExists(self.to.0));
3076 }
3077 if a.move_key(self.from, self.to) {
3078 Ok(())
3079 } else {
3080 Err(ModelError::NoKeyframe(self.from.0))
3081 }
3082 }
3083}
3084
3085struct SetEasingOp {
3086 frame: Frame,
3087 i: Interpolation,
3088 o: EasingHandle,
3089 e: EasingHandle,
3090}
3091impl PropVisitor for SetEasingOp {
3092 type Out = Result<(Interpolation, EasingHandle, EasingHandle), ModelError>;
3093 fn visit<T: PropValue>(self, a: &mut Animated<T>) -> Self::Out {
3094 a.set_easing(self.frame, self.i, self.o, self.e)
3095 .ok_or(ModelError::NoKeyframe(self.frame.0))
3096 }
3097}
3098
3099struct IsAnimatedOp;
3100impl PropReader for IsAnimatedOp {
3101 type Out = bool;
3102 fn read<T: PropValue>(self, a: &Animated<T>) -> bool {
3103 a.has_keys()
3104 }
3105}
3106
3107struct ValueAtOp(f64);
3108impl PropReader for ValueAtOp {
3109 type Out = Value;
3110 fn read<T: PropValue>(self, a: &Animated<T>) -> Value {
3111 a.value_at(self.0).into_value()
3112 }
3113}
3114
3115struct GetStaticOp;
3116impl PropReader for GetStaticOp {
3117 type Out = Value;
3118 fn read<T: PropValue>(self, a: &Animated<T>) -> Value {
3119 a.base.clone().into_value()
3120 }
3121}
3122
3123struct GetKeyOp(Frame);
3124impl PropReader for GetKeyOp {
3125 type Out = Option<KeyframeData>;
3126 fn read<T: PropValue>(self, a: &Animated<T>) -> Option<KeyframeData> {
3127 a.key_at(self.0).map(|k| KeyframeData {
3128 frame: k.frame,
3129 value: k.value.clone().into_value(),
3130 interpolation: k.interpolation,
3131 ease_out: k.ease_out,
3132 ease_in: k.ease_in,
3133 })
3134 }
3135}
3136
3137struct KeyFramesOp;
3139impl PropReader for KeyFramesOp {
3140 type Out = Vec<Frame>;
3141 fn read<T: PropValue>(self, a: &Animated<T>) -> Vec<Frame> {
3142 a.keyframes.iter().map(|k| k.frame).collect()
3143 }
3144}
3145
3146impl Document {
3147 pub fn find_nodes_by_name<'a>(&'a self, name: &'a str) -> impl Iterator<Item = NodeId> + 'a {
3150 self.nodes
3151 .iter()
3152 .filter(move |(_, n)| n.name == name)
3153 .map(|(id, _)| id)
3154 }
3155
3156 fn pm<'a>(&'a mut self, id: NodeId, prop: &PropPath) -> Result<PropMut<'a>, ModelError> {
3157 self.nodes
3158 .get_mut(id)
3159 .ok_or(ModelError::MissingNode)?
3160 .prop_mut(prop)
3161 .ok_or_else(|| ModelError::MissingProp(prop.0.clone()))
3162 }
3163 fn pr<'a>(&'a self, id: NodeId, prop: &PropPath) -> Result<PropRef<'a>, ModelError> {
3164 self.nodes
3165 .get(id)
3166 .ok_or(ModelError::MissingNode)?
3167 .prop_ref(prop)
3168 .ok_or_else(|| ModelError::MissingProp(prop.0.clone()))
3169 }
3170
3171 pub fn set_static(
3173 &mut self,
3174 id: NodeId,
3175 prop: &PropPath,
3176 v: &Value,
3177 ) -> Result<Value, ModelError> {
3178 let name = prop.0.clone();
3179 visit_prop(self.pm(id, prop)?, SetStaticOp(v, &name))
3180 }
3181 pub fn add_keyframe(
3183 &mut self,
3184 id: NodeId,
3185 prop: &PropPath,
3186 frame: Frame,
3187 v: &Value,
3188 ) -> Result<Option<KeyframeData>, ModelError> {
3189 let name = prop.0.clone();
3190 visit_prop(
3191 self.pm(id, prop)?,
3192 AddKeyOp {
3193 frame,
3194 value: v,
3195 prop: &name,
3196 },
3197 )
3198 }
3199 pub fn remove_keyframe(
3200 &mut self,
3201 id: NodeId,
3202 prop: &PropPath,
3203 frame: Frame,
3204 ) -> Result<KeyframeData, ModelError> {
3205 visit_prop(self.pm(id, prop)?, RemoveKeyOp(frame))
3206 }
3207 pub fn restore_keyframe(
3208 &mut self,
3209 id: NodeId,
3210 prop: &PropPath,
3211 key: &KeyframeData,
3212 ) -> Result<(), ModelError> {
3213 let name = prop.0.clone();
3214 visit_prop(self.pm(id, prop)?, RestoreKeyOp(key, &name))
3215 }
3216 pub fn move_keyframe(
3217 &mut self,
3218 id: NodeId,
3219 prop: &PropPath,
3220 from: Frame,
3221 to: Frame,
3222 ) -> Result<(), ModelError> {
3223 visit_prop(self.pm(id, prop)?, MoveKeyOp { from, to })
3224 }
3225 pub fn set_easing(
3227 &mut self,
3228 id: NodeId,
3229 prop: &PropPath,
3230 frame: Frame,
3231 i: Interpolation,
3232 o: EasingHandle,
3233 e: EasingHandle,
3234 ) -> Result<(Interpolation, EasingHandle, EasingHandle), ModelError> {
3235 visit_prop(self.pm(id, prop)?, SetEasingOp { frame, i, o, e })
3236 }
3237
3238 pub fn property_is_animated(&self, id: NodeId, prop: &PropPath) -> bool {
3239 self.pr(id, prop)
3240 .map(|p| read_prop(p, IsAnimatedOp))
3241 .unwrap_or(false)
3242 }
3243 pub fn value_at(&self, id: NodeId, prop: &PropPath, frame: f64) -> Result<Value, ModelError> {
3244 Ok(read_prop(self.pr(id, prop)?, ValueAtOp(frame)))
3245 }
3246 pub fn get_static(&self, id: NodeId, prop: &PropPath) -> Result<Value, ModelError> {
3247 Ok(read_prop(self.pr(id, prop)?, GetStaticOp))
3248 }
3249 pub fn keyframe_data(&self, id: NodeId, prop: &PropPath, frame: Frame) -> Option<KeyframeData> {
3250 self.pr(id, prop)
3251 .ok()
3252 .and_then(|p| read_prop(p, GetKeyOp(frame)))
3253 }
3254 pub fn key_frames(&self, id: NodeId, prop: &PropPath) -> Vec<Frame> {
3256 self.pr(id, prop)
3257 .map(|p| read_prop(p, KeyFramesOp))
3258 .unwrap_or_default()
3259 }
3260}
3261
3262#[cfg(test)]
3263mod tests {
3264 use super::*;
3265 use glam::DVec2;
3266
3267 fn doc_with_ellipse_and_fill() -> (Document, NodeId) {
3268 let mut doc = Document::empty();
3269 let shape = doc.create_node(Node::new(
3270 "e",
3271 NodeKind::Shape(ShapeKind::Ellipse {
3272 pos: Animated::new(DVec2::new(0.0, 0.0)),
3273 size: Animated::new(DVec2::new(100.0, 100.0)),
3274 }),
3275 ));
3276 let fill = doc.create_node(Node::new(
3277 "f",
3278 NodeKind::Style(StyleKind::Fill {
3279 paint: StylePaint::solid(Color::BLACK),
3280 rule: FillRule::NonZero,
3281 }),
3282 ));
3283 doc.attach(shape, Parent::Comp(doc.main), 0).unwrap();
3284 doc.attach(fill, Parent::Comp(doc.main), 1).unwrap();
3285 (doc, shape)
3286 }
3287
3288 #[test]
3289 fn override_beats_keyframes() {
3290 let (doc, shape_id) = doc_with_ellipse_and_fill();
3291 let mut ov = Overrides::default();
3292 ov.set(
3293 shape_id,
3294 PropPath::new("shape.pos"),
3295 Value::DVec2(DVec2::new(99.0, 0.0)),
3296 );
3297 let s = evaluate_with(&doc, doc.main, 0.0, &ov);
3298 assert!(s.items[0].path.bounding_box().center().x > 90.0);
3299 }
3300
3301 #[test]
3302 fn no_overrides_matches_evaluate() {
3303 let (doc, _) = doc_with_ellipse_and_fill();
3304 let a = evaluate(&doc, doc.main, 0.0);
3305 let b = evaluate_with(&doc, doc.main, 0.0, &Overrides::default());
3306 assert_eq!(a, b);
3307 }
3308
3309 #[test]
3310 fn five_point_star_is_closed_with_10_corners() {
3311 let p = star_path(DVec2::ZERO, 5, Some(20.0), 50.0, 0.0);
3312 assert!(matches!(
3313 p.elements().last(),
3314 Some(kurbo::PathEl::ClosePath)
3315 ));
3316 let curves = p
3318 .elements()
3319 .iter()
3320 .filter(|e| matches!(e, kurbo::PathEl::CurveTo(_, _, _)))
3321 .count();
3322 assert_eq!(curves, 10);
3323 }
3324
3325 #[test]
3326 fn polygon_six_points() {
3327 let p = star_path(DVec2::ZERO, 6, None, 40.0, 0.0);
3328 let curves = p
3329 .elements()
3330 .iter()
3331 .filter(|e| matches!(e, kurbo::PathEl::CurveTo(_, _, _)))
3332 .count();
3333 assert_eq!(curves, 6);
3334 }
3335
3336 #[test]
3337 fn star_roundness_changes_path() {
3338 let sharp = star_path(DVec2::ZERO, 5, Some(20.0), 50.0, 0.0);
3339 let rounded = star_path(DVec2::ZERO, 5, Some(20.0), 50.0, 8.0);
3340 assert_ne!(
3341 sharp.elements().len(),
3342 rounded.elements().len(),
3343 "roundness must add fillet segments"
3344 );
3345 assert!(rounded.elements().len() > sharp.elements().len());
3347 }
3348
3349 #[test]
3350 fn star_burst_ignores_inner_radius() {
3351 let star = star_path(DVec2::ZERO, 5, Some(20.0), 50.0, 0.0);
3352 let burst = star_path(DVec2::ZERO, 5, None, 50.0, 0.0);
3353 let star_verts = star
3354 .elements()
3355 .iter()
3356 .filter(|e| matches!(e, kurbo::PathEl::CurveTo(_, _, _)))
3357 .count();
3358 let burst_verts = burst
3359 .elements()
3360 .iter()
3361 .filter(|e| matches!(e, kurbo::PathEl::CurveTo(_, _, _)))
3362 .count();
3363 assert_eq!(star_verts, 10);
3364 assert_eq!(burst_verts, 5);
3365 }
3366
3367 #[test]
3368 fn shape_path_honors_roundness_override() {
3369 let mut doc = Document::empty();
3370 let star = doc.create_node(Node::new(
3371 "star",
3372 NodeKind::Shape(ShapeKind::Star {
3373 pos: Animated::new(DVec2::ZERO),
3374 points: Animated::new(5.0),
3375 inner_r: Animated::new(20.0),
3376 outer_r: Animated::new(50.0),
3377 roundness: Animated::new(0.0),
3378 kind: StarKind::Star,
3379 }),
3380 ));
3381 doc.attach(star, Parent::Comp(doc.main), 0).unwrap();
3382 let shape_kind = match &doc.nodes.get(star).unwrap().kind {
3383 NodeKind::Shape(s) => s.clone(),
3384 _ => unreachable!(),
3385 };
3386 let sharp = shape_path(&shape_kind, star, 0.0, &Overrides::default());
3387 let mut ov = Overrides::default();
3388 ov.set(star, PropPath::new("shape.roundness"), Value::F64(12.0));
3389 let rounded = shape_path(&shape_kind, star, 0.0, &ov);
3390 assert_ne!(
3391 sharp.elements().len(),
3392 rounded.elements().len(),
3393 "roundness override must affect path"
3394 );
3395 }
3396
3397 #[test]
3398 fn stroke_solid_override_uses_stroke_color_path() {
3399 let mut doc = Document::empty();
3400 let comp = doc.main;
3401 let shape = doc.create_node(Node::new(
3402 "s",
3403 NodeKind::Shape(ShapeKind::Ellipse {
3404 pos: Animated::new(DVec2::ZERO),
3405 size: Animated::new(DVec2::splat(100.0)),
3406 }),
3407 ));
3408 let stroke = doc.create_node(Node::new(
3409 "st",
3410 NodeKind::Style(StyleKind::Stroke {
3411 paint: StylePaint::solid(Color::BLACK),
3412 width: Animated::new(4.0),
3413 cap: StrokeCap::Butt,
3414 join: StrokeJoin::Miter,
3415 dash: None,
3416 }),
3417 ));
3418 doc.attach(shape, Parent::Comp(comp), 0).unwrap();
3419 doc.attach(stroke, Parent::Comp(comp), 1).unwrap();
3420 let mut ov = Overrides::default();
3422 ov.set(
3423 stroke,
3424 PropPath::new("stroke.color"),
3425 Value::Color(Color::rgba(1.0, 0.0, 0.0, 1.0)),
3426 );
3427 let scene = evaluate_with(&doc, comp, 0.0, &ov);
3428 assert_eq!(scene.items.len(), 1);
3429 assert_eq!(
3430 scene.items[0].paint,
3431 ScenePaint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0))
3432 );
3433 let mut ov2 = Overrides::default();
3435 ov2.set(
3436 stroke,
3437 PropPath::new("fill.color"),
3438 Value::Color(Color::rgba(0.0, 1.0, 0.0, 1.0)),
3439 );
3440 let scene2 = evaluate_with(&doc, comp, 0.0, &ov2);
3441 assert_eq!(scene2.items[0].paint, ScenePaint::Solid(Color::BLACK));
3442 }
3443
3444 fn find_fill(doc: &Document) -> NodeId {
3445 let mut found = None;
3446 for (id, n) in doc.nodes.iter() {
3447 if let NodeKind::Style(StyleKind::Fill { .. }) = n.kind {
3448 found = Some(id);
3449 }
3450 }
3451 found.unwrap()
3452 }
3453
3454 #[test]
3455 fn text_node_evaluates_to_scene_items() {
3456 let mut doc = Document::empty();
3457 let comp = doc.main;
3458 let text = doc.create_node(Node::new(
3459 "t",
3460 NodeKind::Text(TextNode {
3461 text: "Hi".into(),
3462 size: Animated::new(64.0),
3463 align: TextAlign::Left,
3464 font: None,
3465 }),
3466 ));
3467 let fill = doc.create_node(Node::new(
3468 "f",
3469 NodeKind::Style(StyleKind::Fill {
3470 paint: StylePaint::solid(Color::BLACK),
3471 rule: FillRule::NonZero,
3472 }),
3473 ));
3474 doc.attach(text, Parent::Comp(comp), 0).unwrap();
3475 doc.attach(fill, Parent::Comp(comp), 1).unwrap();
3476 let scene = evaluate(&doc, comp, 0.0);
3477 assert_eq!(scene.items.len(), 1);
3478 assert!(!scene.items[0].path.elements().is_empty());
3479 assert_eq!(scene.items[0].node, text);
3480 }
3481
3482 #[test]
3483 fn font_asset_lookup_finds_family() {
3484 let mut doc = Document::empty();
3485 let id = doc.assets.insert(Asset::Font(FontAsset {
3486 name: "Test".into(),
3487 family: "test".into(),
3488 bytes: vec![1, 2, 3],
3489 }));
3490
3491 let found = doc.font_asset_for_family("test").unwrap();
3492 assert_eq!(found.0, id);
3493 assert_eq!(found.1.family, "test");
3494 assert!(doc.font_asset_for_family("missing").is_none());
3495 }
3496
3497 #[test]
3498 fn font_families_sorted_and_deduped() {
3499 let mut doc = Document::empty();
3500 let a = doc.assets.insert(Asset::Font(FontAsset {
3501 name: "B".into(),
3502 family: "zeta".into(),
3503 bytes: vec![],
3504 }));
3505 let b = doc.assets.insert(Asset::Font(FontAsset {
3506 name: "A".into(),
3507 family: "alpha".into(),
3508 bytes: vec![],
3509 }));
3510 let c = doc.assets.insert(Asset::Font(FontAsset {
3511 name: "dup".into(),
3512 family: "alpha".into(),
3513 bytes: vec![],
3514 }));
3515 doc.asset_order.extend([a, b, c]);
3516 assert_eq!(doc.font_families(), vec!["alpha", "zeta"]);
3517 }
3518
3519 #[test]
3520 fn text_prefers_project_font_family() {
3521 let mut doc = Document::empty();
3522 let comp = doc.main;
3523
3524 let font_bytes = include_bytes!("../../renamite-text/assets/default.ttf").to_vec();
3525 doc.assets.insert(Asset::Font(FontAsset {
3526 name: "Default Test Font".into(),
3527 family: "testfont".into(),
3528 bytes: font_bytes,
3529 }));
3530
3531 let text = doc.create_node(Node::new(
3532 "t",
3533 NodeKind::Text(TextNode {
3534 text: "Hello".into(),
3535 size: Animated::new(48.0),
3536 align: TextAlign::Left,
3537 font: Some("testfont".into()),
3538 }),
3539 ));
3540 let fill = doc.create_node(Node::new(
3541 "f",
3542 NodeKind::Style(StyleKind::Fill {
3543 paint: StylePaint::solid(Color::BLACK),
3544 rule: FillRule::NonZero,
3545 }),
3546 ));
3547 doc.attach(text, Parent::Comp(comp), 0).unwrap();
3548 doc.attach(fill, Parent::Comp(comp), 1).unwrap();
3549
3550 let scene = evaluate(&doc, comp, 0.0);
3551 assert_eq!(scene.items.len(), 1);
3552 assert!(!scene.items[0].path.elements().is_empty());
3553 }
3554
3555 #[test]
3556 fn text_with_unknown_family_falls_back_to_default() {
3557 let mut doc = Document::empty();
3558 let comp = doc.main;
3559 let text = doc.create_node(Node::new(
3560 "t",
3561 NodeKind::Text(TextNode {
3562 text: "Hello".into(),
3563 size: Animated::new(48.0),
3564 align: TextAlign::Left,
3565 font: Some("not-a-font-family".into()),
3566 }),
3567 ));
3568 let fill = doc.create_node(Node::new(
3569 "f",
3570 NodeKind::Style(StyleKind::Fill {
3571 paint: StylePaint::solid(Color::BLACK),
3572 rule: FillRule::NonZero,
3573 }),
3574 ));
3575 doc.attach(text, Parent::Comp(comp), 0).unwrap();
3576 doc.attach(fill, Parent::Comp(comp), 1).unwrap();
3577
3578 let scene = evaluate(&doc, comp, 0.0);
3579 assert_eq!(scene.items.len(), 1);
3580 assert!(!scene.items[0].path.elements().is_empty());
3581 }
3582
3583 #[test]
3584 fn image_layer_emits_ordered_scene_item() {
3585 let mut doc = Document::empty();
3586 let comp = doc.main;
3587
3588 let asset = doc.assets.insert(Asset::Image(ImageAsset {
3589 name: "test.png".into(),
3590 mime: "image/png".into(),
3591 bytes: vec![1, 2, 3],
3592 width: 64,
3593 height: 32,
3594 srgb: true,
3595 }));
3596 doc.asset_order.push(asset);
3597
3598 let mut node = Node::new("Image", NodeKind::Image(asset));
3599 node.transform.anchor = Animated::new(glam::DVec2::new(32.0, 16.0));
3600 node.transform.position = Animated::new(glam::DVec2::new(100.0, 100.0));
3601
3602 let image = doc.create_node(node);
3603 doc.attach(image, Parent::Comp(comp), 0).unwrap();
3604
3605 let scene = evaluate(&doc, comp, 0.0);
3606
3607 assert_eq!(scene.items.len(), 1);
3608 assert_eq!(scene.items[0].node, image);
3609
3610 assert!(matches!(
3611 scene.items[0].paint,
3612 ScenePaint::Image { asset: id, .. } if id == asset
3613 ));
3614 }
3615
3616 #[test]
3617 fn garbage_collect_keeps_referenced_images() {
3618 let mut doc = Document::empty();
3619 let comp = doc.main;
3620
3621 let used = doc.assets.insert(Asset::Image(ImageAsset {
3622 name: "used.png".into(),
3623 mime: "image/png".into(),
3624 bytes: vec![],
3625 width: 1,
3626 height: 1,
3627 srgb: true,
3628 }));
3629 let orphan = doc.assets.insert(Asset::Image(ImageAsset {
3630 name: "orphan.png".into(),
3631 mime: "image/png".into(),
3632 bytes: vec![],
3633 width: 1,
3634 height: 1,
3635 srgb: true,
3636 }));
3637 doc.asset_order.push(used);
3638
3639 let image = doc.create_node(Node::new("Image", NodeKind::Image(used)));
3640 doc.attach(image, Parent::Comp(comp), 0).unwrap();
3641
3642 doc.garbage_collect();
3643
3644 assert!(doc.assets.contains_key(used));
3645 assert!(!doc.assets.contains_key(orphan));
3646 assert_eq!(doc.asset_order, vec![used]);
3647 }
3648
3649 #[test]
3650 fn normalize_assets_repairs_order() {
3651 let mut doc = Document::empty();
3652 let a = doc.assets.insert(Asset::Font(FontAsset {
3653 name: "A".into(),
3654 family: "a".into(),
3655 bytes: vec![],
3656 }));
3657 let b = doc.assets.insert(Asset::Font(FontAsset {
3658 name: "B".into(),
3659 family: "b".into(),
3660 bytes: vec![],
3661 }));
3662 let dangling = doc.assets.insert(Asset::Font(FontAsset {
3664 name: "d".into(),
3665 family: "d".into(),
3666 bytes: vec![],
3667 }));
3668 doc.assets.remove(dangling);
3669 doc.asset_order = vec![a, a, dangling];
3670
3671 doc.normalize_assets();
3672
3673 assert_eq!(doc.asset_order, vec![a, b]);
3674 }
3675
3676 #[test]
3677 fn pick_hits_center_and_misses_outside() {
3678 let (doc, shape) = doc_with_ellipse_and_fill();
3679 let scene = evaluate(&doc, doc.main, 0.0);
3680 assert_eq!(pick(&scene, DVec2::ZERO), Some(shape));
3681 assert_eq!(pick(&scene, DVec2::new(500.0, 500.0)), None);
3682 }
3683
3684 #[test]
3685 fn pick_box_contains_fully() {
3686 let (doc, shape) = doc_with_ellipse_and_fill();
3687 let scene = evaluate(&doc, doc.main, 0.0);
3688 let picked = pick_box(&scene, DVec2::splat(-200.0), DVec2::splat(200.0));
3689 assert_eq!(picked, vec![shape]);
3690 }
3691
3692 #[test]
3693 fn dashed_stroke_gaps_are_not_pickable() {
3694 let mut path = BezPath::new();
3695 path.move_to((0.0, 0.0));
3696 path.line_to((40.0, 0.0));
3697
3698 let shape = NodeId::default();
3699
3700 let scene = Scene {
3701 clips: vec![],
3702 items: vec![SceneItem {
3703 path,
3704 node: shape,
3705 style: NodeId::default(),
3706 paint: ScenePaint::Solid(Color::BLACK),
3707 kind: PaintKind::Stroke(StrokeSample {
3708 width: 4.0,
3709 cap: StrokeCap::Butt,
3710 join: StrokeJoin::Miter,
3711 dash: Some(DashSample {
3712 dashes: vec![10.0, 10.0],
3713 offset: 0.0,
3714 }),
3715 }),
3716 opacity: 1.0,
3717 clips: vec![],
3718 blend: BlendMode::Normal,
3719 }],
3720 };
3721
3722 assert_eq!(pick(&scene, DVec2::new(5.0, 0.0)), Some(shape),);
3723
3724 assert_eq!(
3725 pick(&scene, DVec2::new(15.0, 0.0)),
3726 None,
3727 "point lies inside an off-gap",
3728 );
3729
3730 assert_eq!(pick(&scene, DVec2::new(25.0, 0.0)), Some(shape),);
3731 }
3732
3733 #[test]
3734 fn gradient_fill_emits_linear_paint() {
3735 let (mut doc, _) = doc_with_ellipse_and_fill();
3736 let fill = find_fill(&doc);
3737 let NodeKind::Style(st) = &mut doc.nodes[fill].kind else {
3738 panic!("fill node missing");
3739 };
3740 st.swap_paint(StylePaint::linear(
3741 DVec2::new(0.0, 0.0),
3742 DVec2::new(100.0, 0.0),
3743 GradientStops(vec![
3744 GradientStop {
3745 offset: 0.0,
3746 color: Color::BLACK,
3747 },
3748 GradientStop {
3749 offset: 1.0,
3750 color: Color::WHITE,
3751 },
3752 ]),
3753 ));
3754 let scene = evaluate(&doc, doc.main, 0.0);
3755 let item = &scene.items[0];
3756 match &item.paint {
3757 ScenePaint::LinearGradient { start, end, .. } => {
3758 assert!((start.x - 0.0).abs() < 1e-9);
3759 assert!((end.x - 100.0).abs() < 1e-9);
3760 }
3761 other => panic!("expected linear gradient, got {other:?}"),
3762 }
3763 }
3764
3765 #[test]
3766 fn radial_gradient_keeps_radius_for_identity_transform() {
3767 let (mut doc, _) = doc_with_ellipse_and_fill();
3768 let fill = find_fill(&doc);
3769 let NodeKind::Style(st) = &mut doc.nodes[fill].kind else {
3770 panic!("fill node missing");
3771 };
3772 st.swap_paint(StylePaint::radial(
3773 DVec2::new(0.0, 0.0),
3774 DVec2::new(120.0, 0.0),
3775 GradientStops(vec![GradientStop {
3776 offset: 0.0,
3777 color: Color::WHITE,
3778 }]),
3779 ));
3780 let scene = evaluate(&doc, doc.main, 0.0);
3781 let item = &scene.items[0];
3782 match &item.paint {
3783 ScenePaint::RadialGradient { center, end, .. } => {
3784 assert!((center.x - 0.0).abs() < 1e-9 && (center.y - 0.0).abs() < 1e-9);
3785 assert!((end.x - 120.0).abs() < 1e-9, "radius must not degenerate");
3786 }
3787 other => panic!("expected radial gradient, got {other:?}"),
3788 }
3789 }
3790
3791 #[test]
3792 fn scene_items_carry_the_painting_style_and_fill_style_for_resolves() {
3793 let (doc, shape) = doc_with_ellipse_and_fill();
3794 let fill = find_fill(&doc);
3795 assert_eq!(fill_style_for(&doc, shape), Some(fill));
3796 let scene = evaluate(&doc, doc.main, 0.0);
3797 assert_eq!(scene.items.len(), 1);
3798 assert_eq!(scene.items[0].node, shape);
3799 assert_eq!(scene.items[0].style, fill);
3800 }
3801
3802 #[test]
3803 fn nodes_bounds_unions_selected() {
3804 let (doc, shape) = doc_with_ellipse_and_fill();
3805 let scene = evaluate(&doc, doc.main, 0.0);
3806 let (min, max) = nodes_bounds(&scene, &[shape]).unwrap();
3807 assert!(
3808 min.x <= 0.0 && max.x >= 0.0,
3809 "bounds must cover the ellipse center"
3810 );
3811 }
3812
3813 #[test]
3814 fn dash_entries_are_addressable_properties() {
3815 let mut doc = Document::empty();
3816
3817 let stroke = doc.create_node(Node::new(
3818 "Stroke",
3819 NodeKind::Style(StyleKind::Stroke {
3820 paint: StylePaint::solid(Color::BLACK),
3821 width: Animated::new(4.0),
3822 cap: StrokeCap::Round,
3823 join: StrokeJoin::Round,
3824 dash: Some(AnimatedDash {
3825 dashes: vec![Animated::new(12.0), Animated::new(8.0)],
3826 offset: Animated::new(2.0),
3827 }),
3828 }),
3829 ));
3830
3831 doc.attach(stroke, Parent::Comp(doc.main), 0).unwrap();
3832
3833 assert_eq!(
3834 doc.value_at(stroke, &PropPath::new("stroke.dash.0"), 0.0,)
3835 .unwrap(),
3836 Value::F64(12.0),
3837 );
3838
3839 assert_eq!(
3840 doc.value_at(stroke, &PropPath::new("stroke.dash.offset"), 0.0,)
3841 .unwrap(),
3842 Value::F64(2.0),
3843 );
3844
3845 doc.set_static(stroke, &PropPath::new("stroke.dash.1"), &Value::F64(4.0))
3846 .unwrap();
3847
3848 assert_eq!(
3849 doc.value_at(stroke, &PropPath::new("stroke.dash.1"), 0.0,)
3850 .unwrap(),
3851 Value::F64(4.0),
3852 );
3853 }
3854
3855 #[test]
3856 fn repeater_falloff_fades_copies_linearly() {
3857 let mut doc = Document::empty();
3858 let comp = doc.main;
3859 let group = doc.create_node(Node::new("g", NodeKind::Group));
3860 let shape = doc.create_node(Node::new(
3861 "r",
3862 NodeKind::Shape(ShapeKind::Rect {
3863 pos: Animated::new(DVec2::new(30.0, 30.0)),
3864 size: Animated::new(DVec2::splat(40.0)),
3865 rounded: Animated::new(0.0),
3866 }),
3867 ));
3868 let mut step = AnimatedTransform::identity();
3869 step.position = Animated::new(DVec2::new(60.0, 0.0));
3870 let rep = doc.create_node(Node::new(
3871 "rp",
3872 NodeKind::Modifier(ModifierKind::Repeater {
3873 copies: Animated::new(3.0),
3874 offset: Animated::new(0.0),
3875 transform: step,
3876 start_opacity: Animated::new(1.0),
3877 end_opacity: Animated::new(0.2),
3878 }),
3879 ));
3880 let fill = doc.create_node(Node::new(
3881 "f",
3882 NodeKind::Style(StyleKind::Fill {
3883 paint: StylePaint::solid(Color::BLACK),
3884 rule: FillRule::NonZero,
3885 }),
3886 ));
3887 doc.attach(shape, Parent::Node(group), 0).unwrap();
3888 doc.attach(rep, Parent::Node(group), 1).unwrap();
3889 doc.attach(fill, Parent::Node(group), 2).unwrap();
3890 doc.attach(group, Parent::Comp(comp), 0).unwrap();
3891
3892 let scene = evaluate(&doc, comp, 0.0);
3893 assert_eq!(scene.items.len(), 3);
3894 let ops: Vec<f64> = scene.items.iter().map(|i| i.opacity).collect();
3895 assert!((ops[0] - 1.0).abs() < 1e-9);
3896 assert!(
3897 (ops[1] - 0.6).abs() < 1e-9,
3898 "midpoint of 1.0..0.2, got {}",
3899 ops[1]
3900 );
3901 assert!((ops[2] - 0.2).abs() < 1e-9);
3902 }
3903
3904 #[test]
3905 fn repeater_opacity_props_are_addressable() {
3906 let mut doc = Document::empty();
3907 let id = doc.create_node(Node::new(
3908 "rp",
3909 NodeKind::Modifier(ModifierKind::Repeater {
3910 copies: Animated::new(2.0),
3911 offset: Animated::new(0.0),
3912 transform: AnimatedTransform::identity(),
3913 start_opacity: Animated::new(1.0),
3914 end_opacity: Animated::new(0.25),
3915 }),
3916 ));
3917
3918 doc.attach(id, Parent::Comp(doc.main), 0).unwrap();
3919
3920 assert_eq!(
3921 doc.value_at(id, &PropPath::new("repeater.start_opacity"), 0.0)
3922 .unwrap(),
3923 Value::F64(1.0),
3924 );
3925
3926 assert_eq!(
3927 doc.value_at(id, &PropPath::new("repeater.end_opacity"), 0.0)
3928 .unwrap(),
3929 Value::F64(0.25),
3930 );
3931
3932 doc.set_static(id, &PropPath::new("repeater.end_opacity"), &Value::F64(0.5))
3933 .unwrap();
3934
3935 assert_eq!(
3936 doc.value_at(id, &PropPath::new("repeater.end_opacity"), 0.0)
3937 .unwrap(),
3938 Value::F64(0.5),
3939 );
3940 }
3941
3942 #[test]
3943 fn offset_path_expands_rect_bounds() {
3944 let mut doc = Document::empty();
3945 let comp = doc.main;
3946 let group = doc.create_node(Node::new("g", NodeKind::Group));
3947
3948 let rect = doc.create_node(Node::new(
3949 "r",
3950 NodeKind::Shape(ShapeKind::Rect {
3951 pos: Animated::new(DVec2::new(100.0, 100.0)),
3952 size: Animated::new(DVec2::new(100.0, 100.0)),
3953 rounded: Animated::new(0.0),
3954 }),
3955 ));
3956
3957 let offset = doc.create_node(Node::new(
3958 "op",
3959 NodeKind::Modifier(ModifierKind::OffsetPath {
3960 amount: Animated::new(10.0),
3961 }),
3962 ));
3963
3964 let fill = doc.create_node(Node::new(
3965 "f",
3966 NodeKind::Style(StyleKind::Fill {
3967 paint: StylePaint::solid(Color::WHITE),
3968 rule: FillRule::NonZero,
3969 }),
3970 ));
3971
3972 doc.attach(rect, Parent::Node(group), 0).unwrap();
3973 doc.attach(offset, Parent::Node(group), 1).unwrap();
3974 doc.attach(fill, Parent::Node(group), 2).unwrap();
3975 doc.attach(group, Parent::Comp(comp), 0).unwrap();
3976
3977 let scene = evaluate(&doc, comp, 0.0);
3978 let bb = scene.items[0].path.bounding_box();
3979
3980 assert!(bb.width() > 118.0, "bb = {:?}", bb);
3981 assert!(bb.height() > 118.0, "bb = {:?}", bb);
3982 }
3983
3984 #[test]
3985 fn offset_amount_property_is_addressable() {
3986 let mut doc = Document::empty();
3987
3988 let id = doc.create_node(Node::new(
3989 "op",
3990 NodeKind::Modifier(ModifierKind::OffsetPath {
3991 amount: Animated::new(5.0),
3992 }),
3993 ));
3994
3995 doc.attach(id, Parent::Comp(doc.main), 0).unwrap();
3996
3997 assert_eq!(
3998 doc.value_at(id, &PropPath::new("offset.amount"), 0.0)
3999 .unwrap(),
4000 Value::F64(5.0),
4001 );
4002
4003 doc.set_static(id, &PropPath::new("offset.amount"), &Value::F64(12.0))
4004 .unwrap();
4005
4006 assert_eq!(
4007 doc.value_at(id, &PropPath::new("offset.amount"), 0.0)
4008 .unwrap(),
4009 Value::F64(12.0),
4010 );
4011 }
4012
4013 #[test]
4014 fn zigzag_modifier_perturbs_rect_edge() {
4015 let mut doc = Document::empty();
4016 let comp = doc.main;
4017 let group = doc.create_node(Node::new("g", NodeKind::Group));
4018
4019 let rect = doc.create_node(Node::new(
4020 "r",
4021 NodeKind::Shape(ShapeKind::Rect {
4022 pos: Animated::new(DVec2::new(100.0, 100.0)),
4023 size: Animated::new(DVec2::new(100.0, 100.0)),
4024 rounded: Animated::new(0.0),
4025 }),
4026 ));
4027
4028 let zz = doc.create_node(Node::new(
4029 "zz",
4030 NodeKind::Modifier(ModifierKind::ZigZag {
4031 amplitude: Animated::new(10.0),
4032 frequency: Animated::new(4.0),
4033 smooth: false,
4034 }),
4035 ));
4036
4037 let fill = doc.create_node(Node::new(
4038 "f",
4039 NodeKind::Style(StyleKind::Fill {
4040 paint: StylePaint::solid(Color::WHITE),
4041 rule: FillRule::NonZero,
4042 }),
4043 ));
4044
4045 doc.attach(rect, Parent::Node(group), 0).unwrap();
4046 doc.attach(zz, Parent::Node(group), 1).unwrap();
4047 doc.attach(fill, Parent::Node(group), 2).unwrap();
4048 doc.attach(group, Parent::Comp(comp), 0).unwrap();
4049
4050 let scene = evaluate(&doc, comp, 0.0);
4051 let path = &scene.items[0].path;
4052 let verts = path
4054 .elements()
4055 .iter()
4056 .filter(|e| matches!(e, kurbo::PathEl::LineTo(_) | kurbo::PathEl::MoveTo(_)))
4057 .count();
4058 assert!(verts > 4, "got {} vertices", verts);
4059 }
4060
4061 #[test]
4062 fn pucker_bloat_expands_and_contracts_bounds() {
4063 let mut doc = Document::empty();
4064 let comp = doc.main;
4065 let group = doc.create_node(Node::new("g", NodeKind::Group));
4066
4067 let rect = doc.create_node(Node::new(
4068 "r",
4069 NodeKind::Shape(ShapeKind::Rect {
4070 pos: Animated::new(DVec2::new(100.0, 100.0)),
4071 size: Animated::new(DVec2::new(100.0, 100.0)),
4072 rounded: Animated::new(0.0),
4073 }),
4074 ));
4075
4076 let fill = doc.create_node(Node::new(
4077 "f",
4078 NodeKind::Style(StyleKind::Fill {
4079 paint: StylePaint::solid(Color::WHITE),
4080 rule: FillRule::NonZero,
4081 }),
4082 ));
4083
4084 doc.attach(rect, Parent::Node(group), 0).unwrap();
4085 doc.attach(fill, Parent::Node(group), 2).unwrap();
4086 doc.attach(group, Parent::Comp(comp), 0).unwrap();
4087
4088 let base = evaluate(&doc, comp, 0.0);
4089 let base_bb = base.items[0].path.bounding_box();
4090
4091 let bloat = doc.create_node(Node::new(
4092 "pb",
4093 NodeKind::Modifier(ModifierKind::PuckerBloat {
4094 amount: Animated::new(50.0),
4095 }),
4096 ));
4097 doc.attach(bloat, Parent::Node(group), 1).unwrap();
4098
4099 let bloated = evaluate(&doc, comp, 0.0);
4100 let bloat_bb = bloated.items[0].path.bounding_box();
4101 assert!(
4102 bloat_bb.width() > base_bb.width(),
4103 "w {} vs {}",
4104 bloat_bb.width(),
4105 base_bb.width()
4106 );
4107 assert!(bloat_bb.height() > base_bb.height());
4108
4109 doc.set_static(bloat, &PropPath::new("pucker.amount"), &Value::F64(-50.0))
4110 .unwrap();
4111 let puckered = evaluate(&doc, comp, 0.0);
4112 let pucker_bb = puckered.items[0].path.bounding_box();
4113 assert!(
4116 pucker_bb.width() > bloat_bb.width(),
4117 "pucker {} vs bloat {}",
4118 pucker_bb.width(),
4119 bloat_bb.width()
4120 );
4121 }
4122
4123 #[test]
4124 fn zigzag_props_are_addressable() {
4125 let mut doc = Document::empty();
4126 let id = doc.create_node(Node::new(
4127 "zz",
4128 NodeKind::Modifier(ModifierKind::ZigZag {
4129 amplitude: Animated::new(5.0),
4130 frequency: Animated::new(3.0),
4131 smooth: true,
4132 }),
4133 ));
4134 doc.attach(id, Parent::Comp(doc.main), 0).unwrap();
4135
4136 assert_eq!(
4137 doc.value_at(id, &PropPath::new("zigzag.amplitude"), 0.0)
4138 .unwrap(),
4139 Value::F64(5.0),
4140 );
4141 assert_eq!(
4142 doc.value_at(id, &PropPath::new("zigzag.frequency"), 0.0)
4143 .unwrap(),
4144 Value::F64(3.0),
4145 );
4146
4147 doc.set_static(id, &PropPath::new("zigzag.amplitude"), &Value::F64(12.0))
4148 .unwrap();
4149 assert_eq!(
4150 doc.value_at(id, &PropPath::new("zigzag.amplitude"), 0.0)
4151 .unwrap(),
4152 Value::F64(12.0),
4153 );
4154 }
4155
4156 #[test]
4157 fn pucker_amount_property_is_addressable() {
4158 let mut doc = Document::empty();
4159 let id = doc.create_node(Node::new(
4160 "pb",
4161 NodeKind::Modifier(ModifierKind::PuckerBloat {
4162 amount: Animated::new(20.0),
4163 }),
4164 ));
4165 doc.attach(id, Parent::Comp(doc.main), 0).unwrap();
4166
4167 assert_eq!(
4168 doc.value_at(id, &PropPath::new("pucker.amount"), 0.0)
4169 .unwrap(),
4170 Value::F64(20.0),
4171 );
4172
4173 doc.set_static(id, &PropPath::new("pucker.amount"), &Value::F64(-30.0))
4174 .unwrap();
4175 assert_eq!(
4176 doc.value_at(id, &PropPath::new("pucker.amount"), 0.0)
4177 .unwrap(),
4178 Value::F64(-30.0),
4179 );
4180 }
4181}
4182
4183#[cfg(test)]
4184mod trim_tests {
4185 use super::*;
4186 use kurbo::Shape;
4187
4188 fn line() -> BezPath {
4189 let mut p = BezPath::new();
4190 p.move_to((0.0, 0.0));
4191 p.line_to((100.0, 0.0));
4192 p
4193 }
4194
4195 #[test]
4196 fn trim_first_quarter_of_line() {
4197 let out = trim_path(&line(), 0.0, 0.25, 0.0).unwrap();
4198 let bb = out.bounding_box();
4199 assert!((bb.x1 - 25.0).abs() < 0.5, "x1={}", bb.x1);
4200 assert!(bb.x0.abs() < 0.5);
4201 }
4202
4203 #[test]
4204 fn trim_second_half_of_line() {
4205 let out = trim_path(&line(), 0.5, 1.0, 0.0).unwrap();
4206 let bb = out.bounding_box();
4207 assert!((bb.x0 - 50.0).abs() < 0.5 && (bb.x1 - 100.0).abs() < 0.5);
4208 }
4209
4210 #[test]
4211 fn trim_zero_length_returns_none() {
4212 assert!(trim_path(&line(), 0.5, 0.5, 0.0).is_none());
4213 }
4214
4215 #[test]
4216 fn trim_offset_shifts_range() {
4217 let a = trim_path(&line(), 0.0, 0.5, 0.0).unwrap();
4218 let b = trim_path(&line(), 0.0, 0.5, 0.5).unwrap();
4219 assert!((a.bounding_box().x1 - 50.0).abs() < 0.5);
4220 assert!(
4221 (b.bounding_box().x0 - 50.0).abs() < 0.5,
4222 "offset must shift to second half"
4223 );
4224 }
4225
4226 #[test]
4227 fn trim_wraps_when_offset_pushes_past_end() {
4228 let out = trim_path(&line(), 0.0, 0.5, 0.75).unwrap();
4230 let bb = out.bounding_box();
4231 assert!(bb.x0 < 1.0 && bb.x1 > 99.0, "both ends present");
4232 let moves = out
4234 .elements()
4235 .iter()
4236 .filter(|el| matches!(el, kurbo::PathEl::MoveTo(_)))
4237 .count();
4238 assert_eq!(moves, 2);
4239 }
4240
4241 #[test]
4242 fn trim_quarter_of_closed_square_is_one_side() {
4243 let sq = kurbo::Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1);
4244 let out = trim_path(&sq, 0.0, 0.25, 0.0).unwrap();
4245 let bb = out.bounding_box();
4246 assert!(bb.width().min(bb.height()) < 1.0);
4248 assert!((bb.width().max(bb.height()) - 100.0).abs() < 1.0);
4249 }
4250
4251 #[test]
4252 fn full_range_with_offset_emits_whole_path() {
4253 let out = trim_path(&line(), 0.0, 1.0, 0.3).unwrap();
4254 let bb = out.bounding_box();
4255 assert!(bb.x0 < 0.5 && bb.x1 > 99.5);
4256 }
4257}
4258
4259#[cfg(test)]
4260mod style_paint_tests {
4261 use super::*;
4262 use glam::DVec2;
4263
4264 #[test]
4265 fn paint_snapshot_samples_without_copying_keys() {
4266 let mut color = Animated::new(Color::BLACK);
4267 color.set_key(Frame(0), Color::BLACK);
4268 color.set_key(Frame(10), Color::WHITE);
4269
4270 let sampled = StylePaint::Solid { color }.snapshot(10.0);
4271 let StylePaint::Solid { color } = sampled else {
4272 panic!("expected solid");
4273 };
4274
4275 assert_eq!(color.base, Color::WHITE);
4276 assert!(color.keyframes.is_empty());
4277 }
4278
4279 #[test]
4280 fn set_base_color_preserves_gradient() {
4281 let mut paint =
4282 StylePaint::linear(glam::DVec2::ZERO, glam::DVec2::X, GradientStops::default());
4283
4284 let red = Color::rgba(1.0, 0.0, 0.0, 1.0);
4285 paint.set_base_color(red);
4286
4287 let StylePaint::Gradient(gradient) = paint else {
4288 panic!("must remain a gradient");
4289 };
4290 assert_eq!(gradient.stops.base.0[0].color, red);
4291 }
4292
4293 fn doc_with_square_and_fill() -> (Document, NodeId) {
4294 let mut doc = Document::empty();
4295 let rect = doc.create_node(Node::new(
4296 "r",
4297 NodeKind::Shape(ShapeKind::Rect {
4298 pos: Animated::new(DVec2::new(0.0, 0.0)),
4299 size: Animated::new(DVec2::new(200.0, 200.0)),
4300 rounded: Animated::new(0.0),
4301 }),
4302 ));
4303 let fill = doc.create_node(Node::new(
4304 "f",
4305 NodeKind::Style(StyleKind::Fill {
4306 paint: StylePaint::solid(Color::WHITE),
4307 rule: FillRule::NonZero,
4308 }),
4309 ));
4310 doc.attach(rect, Parent::Comp(doc.main), 0).unwrap();
4311 doc.attach(fill, Parent::Comp(doc.main), 1).unwrap();
4312 (doc, rect)
4313 }
4314
4315 #[test]
4316 fn mask_clips_subsequent_siblings() {
4317 let (mut doc, square) = doc_with_square_and_fill();
4318 let mask = doc.create_node(Node::new(
4319 "m",
4320 NodeKind::Mask(MaskProps {
4321 inverted: false,
4322 shape: ShapeKind::Ellipse {
4323 pos: Animated::new(DVec2::new(100.0, 100.0)),
4324 size: Animated::new(DVec2::new(50.0, 50.0)),
4325 },
4326 }),
4327 ));
4328 let comp = doc.main;
4329 let (_, _) = doc.detach(square).unwrap();
4331 doc.attach(mask, Parent::Comp(comp), 0).unwrap();
4332 doc.attach(square, Parent::Comp(comp), 1).unwrap();
4333
4334 let scene = evaluate(&doc, comp, 0.0);
4335 assert_eq!(scene.items.len(), 1);
4336 let item = &scene.items[0];
4337 assert_eq!(item.node, square);
4338 assert_eq!(item.clips.len(), 1);
4339 assert_eq!(scene.clips.len(), 1);
4340 assert_eq!(scene.clips[0].rule, FillRule::NonZero);
4341 }
4342
4343 #[test]
4344 fn inverted_mask_uses_evenodd_rule() {
4345 let (mut doc, square) = doc_with_square_and_fill();
4346 let mask = doc.create_node(Node::new(
4347 "m",
4348 NodeKind::Mask(MaskProps {
4349 inverted: true,
4350 shape: ShapeKind::Rect {
4351 pos: Animated::new(DVec2::new(100.0, 100.0)),
4352 size: Animated::new(DVec2::new(50.0, 50.0)),
4353 rounded: Animated::new(0.0),
4354 },
4355 }),
4356 ));
4357 let comp = doc.main;
4358 let (_, _) = doc.detach(square).unwrap();
4359 doc.attach(mask, Parent::Comp(comp), 0).unwrap();
4360 doc.attach(square, Parent::Comp(comp), 1).unwrap();
4361
4362 let scene = evaluate(&doc, comp, 0.0);
4363 assert_eq!(scene.items.len(), 1);
4364 assert_eq!(scene.items[0].node, square);
4365 assert_eq!(scene.clips.len(), 1);
4366 assert_eq!(scene.clips[0].rule, FillRule::EvenOdd);
4367 }
4368
4369 #[test]
4370 fn mask_clips_image_siblings() {
4371 let mut doc = Document::empty();
4372 let comp = doc.main;
4373 let mask = doc.create_node(Node::new(
4374 "m",
4375 NodeKind::Mask(MaskProps {
4376 inverted: false,
4377 shape: ShapeKind::Rect {
4378 pos: Animated::new(DVec2::new(0.0, 0.0)),
4379 size: Animated::new(DVec2::new(10.0, 10.0)),
4380 rounded: Animated::new(0.0),
4381 },
4382 }),
4383 ));
4384 let img_asset = doc.assets.insert(Asset::Image(ImageAsset {
4385 name: "img".into(),
4386 mime: "image/png".into(),
4387 bytes: Vec::new(),
4388 width: 64,
4389 height: 64,
4390 srgb: true,
4391 }));
4392 let img = doc.create_node(Node::new("i", NodeKind::Image(img_asset)));
4393 doc.attach(mask, Parent::Comp(comp), 0).unwrap();
4394 doc.attach(img, Parent::Comp(comp), 1).unwrap();
4395
4396 let scene = evaluate(&doc, comp, 0.0);
4397 assert_eq!(scene.items.len(), 1);
4398 assert_eq!(scene.items[0].clips.len(), 1);
4399 }
4400
4401 #[test]
4402 fn mask_param_paths_edit_mask_geometry() {
4403 let mut doc = Document::empty();
4404 let mask = doc.create_node(Node::new(
4405 "m",
4406 NodeKind::Mask(MaskProps {
4407 inverted: false,
4408 shape: ShapeKind::Rect {
4409 pos: Animated::new(DVec2::new(0.0, 0.0)),
4410 size: Animated::new(DVec2::new(10.0, 20.0)),
4411 rounded: Animated::new(0.0),
4412 },
4413 }),
4414 ));
4415 let mut node = doc.nodes.get_mut(mask).unwrap();
4416 let Some(PropMut::Vec2(v)) = node.prop_mut(&PropPath::new("shape.pos")) else {
4417 panic!("mask shape.pos not addressable");
4418 };
4419 v.base = DVec2::new(5.0, 5.0);
4420 drop(node);
4421 let n = doc.nodes.get_mut(mask).unwrap();
4422 let Some(PropRef::Vec2(v)) = n.prop_ref(&PropPath::new("shape.pos")) else {
4423 panic!("mask shape.pos not readable");
4424 };
4425 assert_eq!(v.base, DVec2::new(5.0, 5.0));
4426 }
4427}
4428
4429#[cfg(test)]
4430mod group_transform_tests {
4431 use super::*;
4432
4433 fn grouped_rect() -> (Document, NodeId, NodeId) {
4434 let mut doc = Document::empty();
4435 let comp = doc.main;
4436
4437 let group = doc.create_node(Node::new("Group", NodeKind::Group));
4438
4439 let shape = doc.create_node(Node::new(
4440 "Rect",
4441 NodeKind::Shape(ShapeKind::Rect {
4442 pos: Animated::new(glam::DVec2::new(100.0, 80.0)),
4443 size: Animated::new(glam::DVec2::new(60.0, 40.0)),
4444 rounded: Animated::new(0.0),
4445 }),
4446 ));
4447
4448 let fill = doc.create_node(Node::new(
4449 "Fill",
4450 NodeKind::Style(StyleKind::Fill {
4451 paint: StylePaint::solid(Color::BLACK),
4452 rule: FillRule::NonZero,
4453 }),
4454 ));
4455
4456 doc.attach(shape, Parent::Node(group), 0).unwrap();
4457 doc.attach(fill, Parent::Node(group), 1).unwrap();
4458 doc.attach(group, Parent::Comp(comp), 0).unwrap();
4459
4460 (doc, group, shape)
4461 }
4462
4463 #[test]
4464 fn group_selection_bounds_include_descendants() {
4465 let (doc, group, _) = grouped_rect();
4466 let scene = evaluate(&doc, doc.main, 0.0);
4467
4468 let bounds = selection_bounds(&doc, &scene, &[group]);
4469
4470 assert!(bounds.is_some());
4471
4472 let (min, max) = bounds.unwrap();
4473 assert!(max.x > min.x);
4474 assert!(max.y > min.y);
4475 }
4476
4477 #[test]
4478 fn selected_group_is_resolved_from_child_pick() {
4479 let (doc, group, shape) = grouped_rect();
4480
4481 assert_eq!(
4482 selected_ancestor_for_pick(&doc, shape, &[group]),
4483 Some(group),
4484 );
4485 }
4486
4487 #[test]
4488 fn immediate_child_resolution_descends_one_level() {
4489 let mut doc = Document::empty();
4490
4491 let outer = doc.create_node(Node::new("Outer", NodeKind::Group));
4492 let inner = doc.create_node(Node::new("Inner", NodeKind::Group));
4493 let shape = doc.create_node(Node::new(
4494 "Shape",
4495 NodeKind::Shape(ShapeKind::Ellipse {
4496 pos: Animated::new(glam::DVec2::ZERO),
4497 size: Animated::new(glam::DVec2::ONE),
4498 }),
4499 ));
4500
4501 doc.attach(shape, Parent::Node(inner), 0).unwrap();
4502 doc.attach(inner, Parent::Node(outer), 0).unwrap();
4503 doc.attach(outer, Parent::Comp(doc.main), 0).unwrap();
4504
4505 assert_eq!(immediate_child_below(&doc, outer, shape), Some(inner));
4506 }
4507
4508 #[test]
4509 fn nested_parent_delta_conversion_respects_scale() {
4510 let (mut doc, group, shape) = grouped_rect();
4511
4512 doc.nodes[group].transform.scale = Animated::new(glam::DVec2::splat(200.0));
4513
4514 let local = world_delta_to_parent(&doc, shape, 0.0, glam::DVec2::new(20.0, 10.0)).unwrap();
4515
4516 assert!((local - glam::DVec2::new(10.0, 5.0)).length() < 1e-9);
4517 }
4518}