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