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