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