motion_content/svg/
view.rs1use super::*;
4use motion_core::{
5 Canvas, DeferredContent, DrawContext, Drawable, Node, NodeModifiers, ValidationError,
6};
7
8type Selection = Arc<dyn Fn(Node) -> Node + Send + Sync>;
9
10#[derive(Clone)]
11enum Input {
12 Source(motion_core::Source),
13 Lazy(motion_core::Handle<SvgAsset>),
14 Loaded(Arc<SvgAsset>),
15}
16
17#[derive(Clone)]
20pub struct Svg {
21 input: Input,
22 size: Option<Size>,
23 progress: Option<(f64, crate::WriteOptions)>,
24 selections: Vec<(Arc<str>, Selection)>,
25}
26
27impl Svg {
28 pub fn open(source: impl Into<motion_core::Source>) -> Self {
30 Self {
31 input: Input::Source(source.into()),
32 size: None,
33 progress: None,
34 selections: Vec::new(),
35 }
36 }
37
38 pub fn new(handle: motion_core::Handle<SvgAsset>) -> Self {
40 Self {
41 input: Input::Lazy(handle),
42 size: None,
43 progress: None,
44 selections: Vec::new(),
45 }
46 }
47
48 pub fn from_asset(asset: Arc<SvgAsset>) -> Self {
50 Self {
51 input: Input::Loaded(asset),
52 size: None,
53 progress: None,
54 selections: Vec::new(),
55 }
56 }
57
58 pub fn fit(mut self, size: Size) -> Self {
60 self.size = Some(size);
61 self
62 }
63
64 pub fn write(mut self, progress: f64) -> Self {
66 self.progress = Some((progress, crate::WriteOptions::default()));
67 self
68 }
69
70 pub fn write_with(mut self, progress: f64, options: crate::WriteOptions) -> Self {
72 self.progress = Some((progress, options));
73 self
74 }
75
76 pub fn select(
80 mut self,
81 id: impl Into<Arc<str>>,
82 callback: impl Fn(Node) -> Node + Send + Sync + 'static,
83 ) -> Self {
84 let id = id.into();
85 self.selections.push((
86 Arc::from(id.strip_prefix('#').unwrap_or(&id)),
87 Arc::new(callback),
88 ));
89 self
90 }
91
92 fn asset(&self, context: &DrawContext) -> motion_core::Result<Arc<SvgAsset>> {
93 match &self.input {
94 Input::Source(source) => context
95 .assets()
96 .load::<SvgAsset>(source.clone())
97 .get_with(context.diagnostics()),
98 Input::Lazy(handle) => handle.get_with(context.diagnostics()),
99 Input::Loaded(asset) => Ok(asset.clone()),
100 }
101 }
102
103 pub fn vector_tree(&self) -> motion_core::Result<Arc<VectorTree>> {
105 Ok(self.asset(&DrawContext::default())?.tree.clone())
106 }
107
108 pub fn try_node(&self) -> motion_core::Result<Node> {
110 self.node_with(&DrawContext::default())
111 }
112
113 fn node_with(&self, context: &DrawContext) -> motion_core::Result<Node> {
114 let asset = self.asset(context)?;
115 let size = self.size.unwrap_or(asset.size());
116 if !size.width.is_finite()
117 || !size.height.is_finite()
118 || size.width <= 0.0
119 || size.height <= 0.0
120 || self.selections.len() > 256
121 {
122 return Err(unsupported(
123 "SVG fit requires positive finite size; at most 256 selections",
124 ));
125 }
126 for (id, _) in &self.selections {
127 if !asset.tree.ids().contains_key(id.as_ref()) {
128 return Err(unsupported(format!("SVG selection ID not found: {id}")));
129 }
130 }
131 let mut callbacks_by_address =
132 std::collections::BTreeMap::<Vec<usize>, Vec<&Selection>>::new();
133 for (id, callback) in &self.selections {
134 callbacks_by_address
135 .entry(asset.tree.ids()[id.as_ref()].clone())
136 .or_default()
137 .push(callback);
138 }
139 let map = |address: &[usize], mut node: Node| {
140 if let Some(callbacks) = callbacks_by_address.get(address) {
141 for callback in callbacks {
142 node = callback(node);
143 }
144 }
145 node
146 };
147 let mut node = if let Some((progress, options)) = self.progress {
148 asset.tree.write_mapped(progress, options, &map)?
149 } else {
150 asset.tree.node_mapped(&map)
151 };
152 let scale = (size.width / asset.size().width).min(size.height / asset.size().height);
153 let transform = Transform::scale(scale).then(Transform::translate(
154 (size.width - asset.size().width * scale) * 0.5,
155 (size.height - asset.size().height * scale) * 0.5,
156 ));
157 node = node
158 .clip_with(asset.size().to_rect(), motion_core::ClipMode::Geometric)
159 .transformed(transform)
160 .clip_with(size.to_rect(), motion_core::ClipMode::Geometric);
161 Ok(Node::group([node, Node::from(Viewport(size.to_rect()))])
162 .pivot(motion_core::Pivot::Point(size.to_rect().center())))
163 }
164}
165
166#[derive(Clone)]
167struct Viewport(Rect);
168
169impl Drawable for Viewport {
170 fn bounds(&self, _: &DrawContext) -> Result<Rect, ValidationError> {
171 Ok(self.0)
172 }
173
174 fn draw(&self, _: &DrawContext, _: &mut Canvas<'_>) -> Result<(), ValidationError> {
175 Ok(())
176 }
177}
178
179#[derive(Clone)]
180struct FailedResource(Svg);
181
182impl Drawable for FailedResource {
183 fn bounds(&self, context: &DrawContext) -> Result<Rect, ValidationError> {
184 let _ = self.0.asset(context);
185 Ok(Rect::new(
186 0.0,
187 0.0,
188 self.0.size.map_or(64.0, |size| size.width),
189 self.0.size.map_or(64.0, |size| size.height),
190 ))
191 }
192
193 fn draw(&self, context: &DrawContext, canvas: &mut Canvas<'_>) -> Result<(), ValidationError> {
194 let _ = self.0.asset(context);
195 canvas.fill(
196 &self.bounds(context)?,
197 Color::rgb(255, 0, 255),
198 motion_core::FillRule::NonZero,
199 )
200 }
201}
202
203#[derive(Clone)]
204struct DeferredSvg(Svg);
205
206impl DeferredContent for DeferredSvg {
207 fn resolve(&self, context: &DrawContext) -> Result<Node, ValidationError> {
208 match self.0.node_with(context) {
209 Ok(node) => Ok(node),
210 Err(error) => {
211 if error.kind() == motion_core::ErrorKind::Asset {
212 Ok(Node::from(FailedResource(self.0.clone())))
213 } else {
214 Err(ValidationError::invalid(
215 "svg",
216 "content",
217 error.to_string(),
218 ))
219 }
220 }
221 }
222 }
223}
224
225impl From<Svg> for Node {
226 fn from(svg: Svg) -> Node {
227 Node::deferred(DeferredSvg(svg))
228 }
229}