Skip to main content

ranim_items/vitem/
mod.rs

1//! Quadratic Bezier Concatenated Item
2//!
3//! VItem itself is composed with 3d bezier path segments, but when *ranim* renders VItem,
4//! it assumes that all points are in the same plane to calculate depth information.
5//! Which means that ranim actually renders a **projection** of the VItem onto a plane.
6//!
7//! The projection target plane has the initial basis and normal defined as `(DVec3::X, DVec3::Y)` and `DVec3::Z` respectively, and it contains the first point of the VItem.
8//!
9//! So the normal way to use a [`VItem`] is to make sure that all points are in the same plane, at this time the **projection** is equivalent to the VItem itself. Or you may break this, and let ranim renders the **projection** of it.
10// pub mod arrow;
11/// Geometry items
12pub mod geometry;
13/// Svg item
14pub mod svg;
15/// Simple text items
16#[cfg(feature = "typst")]
17#[cfg_attr(docsrs, doc(cfg(feature = "typst")))]
18pub mod text;
19/// Typst items
20#[cfg(feature = "typst")]
21#[cfg_attr(docsrs, doc(cfg(feature = "typst")))]
22pub mod typst;
23
24use color::{AlphaColor, Srgb, palette::css};
25use glam::{DVec3, Mat4, Vec4, vec4};
26use ranim_core::anchor::Aabb;
27use ranim_core::core_item::CoreItem;
28use ranim_core::{Extract, color, glam};
29
30use ranim_core::{
31    components::{PointVec, VecResizeTrait, rgba::Rgba, vpoint::VPointVec, width::Width},
32    prelude::{Alignable, Empty, FillColor, Opacity, Partial, StrokeWidth},
33    traits::{ApplyTransform, PointsFunc, StrokeColor},
34};
35
36/// A vectorized item.
37///
38/// It is built from four components:
39/// - [`VItem::vpoints`]: the vpoints of the item, see [`VPointVec`].
40/// - [`VItem::stroke_widths`]: the stroke widths of the item, see [`Width`].
41/// - [`VItem::stroke_rgbas`]: the stroke colors of the item, see [`Rgba`].
42/// - [`VItem::fill_rgbas`]: the fill colors of the item, see [`Rgba`].
43///
44/// You can construct a [`VItem`] from a list of VPoints, see [`VPointVec`]:
45///
46/// ```rust
47/// use ranim_core::glam::dvec3;
48/// use ranim_items::vitem::VItem;
49///
50/// let vitem = VItem::from_vpoints(vec![
51///     dvec3(0.0, 0.0, 0.0),
52///     dvec3(1.0, 0.0, 0.0),
53///     dvec3(0.5, 1.0, 0.0),
54/// ]);
55/// ```
56#[derive(Debug, Clone, PartialEq)]
57pub struct VItem {
58    /// The normal vector of the projection target plane.
59    /// If `None`, the normal will be derived from the points at render time.
60    pub normal: Option<DVec3>,
61    /// vpoints data
62    pub vpoints: VPointVec,
63    /// stroke widths
64    pub stroke_widths: PointVec<Width>,
65    /// stroke rgbas
66    pub stroke_rgbas: PointVec<Rgba>,
67    /// fill rgbas
68    pub fill_rgbas: PointVec<Rgba>,
69}
70
71impl ranim_core::traits::Interpolatable for VItem {
72    fn lerp(&self, target: &Self, t: f64) -> Self {
73        Self {
74            normal: match (self.normal, target.normal) {
75                (Some(a), Some(b)) => Some(a.lerp(b, t)),
76                (Some(a), None) => Some(a),
77                (None, Some(b)) => Some(b),
78                (None, None) => None,
79            },
80            vpoints: self.vpoints.lerp(&target.vpoints, t),
81            stroke_widths: self.stroke_widths.lerp(&target.stroke_widths, t),
82            stroke_rgbas: self.stroke_rgbas.lerp(&target.stroke_rgbas, t),
83            fill_rgbas: self.fill_rgbas.lerp(&target.fill_rgbas, t),
84        }
85    }
86}
87
88impl PointsFunc for VItem {
89    fn apply_points_func(&mut self, f: impl Fn(&mut [DVec3])) -> &mut Self {
90        self.vpoints.apply_points_func(f);
91        self
92    }
93}
94
95impl Aabb for VItem {
96    fn aabb(&self) -> [DVec3; 2] {
97        self.vpoints.aabb()
98    }
99}
100
101impl<G: Into<glam::DAffine3>> ApplyTransform<G> for VItem {
102    fn apply(&mut self, transform: G) -> &mut Self {
103        let transform = transform.into();
104        self.vpoints.apply(transform);
105        if let Some(ref mut normal) = self.normal {
106            let matrix = transform.matrix3;
107            let transformed = if matrix.determinant().abs() > 1e-12 {
108                matrix.inverse().transpose() * *normal
109            } else {
110                matrix * *normal
111            };
112            if let Some(unit) = transformed.try_normalize() {
113                *normal = unit;
114            }
115        }
116        self
117    }
118}
119
120/// Default stroke width
121pub use ranim_core::core_item::vitem::DEFAULT_STROKE_WIDTH;
122
123impl VItem {
124    /// Close the VItem
125    pub fn close(&mut self) -> &mut Self {
126        if self.vpoints.last() != self.vpoints.first() && !self.vpoints.is_empty() {
127            let start = self.vpoints[0];
128            let end = self.vpoints[self.vpoints.len() - 1];
129            self.extend_vpoints(&[(start + end) / 2.0, start]);
130        }
131        self
132    }
133    /// Shrink to center
134    pub fn shrink(&mut self) -> &mut Self {
135        let bb = self.aabb();
136        self.vpoints.0 = vec![bb[1]; self.vpoints.len()];
137        self
138    }
139    /// Set the vpoints of the VItem
140    pub fn set_points(&mut self, vpoints: Vec<DVec3>) {
141        self.vpoints.0 = vpoints;
142    }
143    /// Get anchor points
144    pub fn get_anchor(&self, idx: usize) -> Option<&DVec3> {
145        self.vpoints.get(idx * 2)
146    }
147    /// Set the normal of the VItem's projection plane
148    pub fn with_normal(mut self, normal: DVec3) -> Self {
149        self.normal = Some(normal);
150        self
151    }
152    /// Set the normal of the VItem's projection plane
153    pub fn set_normal(&mut self, normal: DVec3) {
154        self.normal = Some(normal);
155    }
156    /// Construct a [`VItem`] form vpoints
157    pub fn from_vpoints(vpoints: Vec<DVec3>) -> Self {
158        let stroke_widths = vec![DEFAULT_STROKE_WIDTH.into(); vpoints.len().div_ceil(2)];
159        let stroke_rgbas = vec![vec4(1.0, 1.0, 1.0, 1.0).into(); vpoints.len().div_ceil(2)];
160        let fill_rgbas = vec![vec4(0.0, 0.0, 0.0, 0.0).into(); vpoints.len().div_ceil(2)];
161        Self {
162            normal: None,
163            vpoints: VPointVec(vpoints),
164            stroke_rgbas: stroke_rgbas.into(),
165            stroke_widths: stroke_widths.into(),
166            fill_rgbas: fill_rgbas.into(),
167        }
168    }
169    /// Extend vpoints of the VItem
170    pub fn extend_vpoints(&mut self, vpoints: &[DVec3]) {
171        self.vpoints.extend(vpoints.to_vec());
172
173        let len = self.vpoints.len();
174        self.fill_rgbas.resize_with_last(len.div_ceil(2));
175        self.stroke_rgbas.resize_with_last(len.div_ceil(2));
176        self.stroke_widths.resize_with_last(len.div_ceil(2));
177    }
178
179    pub(crate) fn get_render_points(&self) -> Vec<Vec4> {
180        self.vpoints
181            .iter()
182            .zip(self.vpoints.get_closepath_flags())
183            .map(|(p, f)| p.as_vec3().extend(f.into()))
184            .collect()
185    }
186    /// Put start and end on
187    pub fn put_start_and_end_on(&mut self, start: DVec3, end: DVec3) -> &mut Self {
188        self.vpoints.put_start_and_end_on(start, end);
189        self
190    }
191}
192
193impl From<VItem> for ranim_core::core_item::vitem::VItem {
194    fn from(value: VItem) -> Self {
195        Self {
196            normal: value.normal.map(|n| n.as_vec3()),
197            points: value.get_render_points(),
198            transform: Mat4::IDENTITY,
199            fill_rgbas: value.fill_rgbas.iter().cloned().collect(),
200            stroke_rgbas: value.stroke_rgbas.iter().cloned().collect(),
201            stroke_widths: value.stroke_widths.iter().cloned().collect(),
202        }
203    }
204}
205
206impl Extract for VItem {
207    type Target = CoreItem;
208    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
209        ranim_core::core_item::vitem::VItem::from(self.clone()).extract_into(buf);
210    }
211}
212
213// MARK: Anim traits impl
214impl Alignable for VItem {
215    fn is_aligned(&self, other: &Self) -> bool {
216        self.vpoints.is_aligned(&other.vpoints)
217            && self.stroke_widths.is_aligned(&other.stroke_widths)
218            && self.stroke_rgbas.is_aligned(&other.stroke_rgbas)
219            && self.fill_rgbas.is_aligned(&other.fill_rgbas)
220    }
221    fn align_with(&mut self, other: &mut Self) {
222        self.vpoints.align_with(&mut other.vpoints);
223        let len = self.vpoints.len().div_ceil(2);
224        self.stroke_rgbas.resize_preserving_order(len);
225        other.stroke_rgbas.resize_preserving_order(len);
226        self.stroke_widths.resize_preserving_order(len);
227        other.stroke_widths.resize_preserving_order(len);
228        self.fill_rgbas.resize_preserving_order(len);
229        other.fill_rgbas.resize_preserving_order(len);
230    }
231}
232
233impl Opacity for VItem {
234    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
235        self.stroke_rgbas.set_opacity(opacity);
236        self.fill_rgbas.set_opacity(opacity);
237        self
238    }
239}
240
241impl Partial for VItem {
242    fn get_partial(&self, range: std::ops::Range<f64>) -> Self {
243        let vpoints = self.vpoints.get_partial(range.clone());
244        let stroke_rgbas = self.stroke_rgbas.get_partial(range.clone());
245        let stroke_widths = self.stroke_widths.get_partial(range.clone());
246        let fill_rgbas = self.fill_rgbas.get_partial(range.clone());
247        Self {
248            normal: self.normal,
249            vpoints,
250            stroke_widths,
251            stroke_rgbas,
252            fill_rgbas,
253        }
254    }
255    fn get_partial_closed(&self, range: std::ops::Range<f64>) -> Self {
256        let mut partial = self.get_partial(range);
257        partial.close();
258        partial
259    }
260}
261
262impl Empty for VItem {
263    fn empty() -> Self {
264        Self {
265            normal: None,
266            vpoints: VPointVec(vec![DVec3::ZERO; 3]),
267            stroke_widths: vec![0.0.into(); 2].into(),
268            stroke_rgbas: vec![Vec4::ZERO.into(); 2].into(),
269            fill_rgbas: vec![Vec4::ZERO.into(); 2].into(),
270        }
271    }
272}
273
274impl FillColor for VItem {
275    fn fill_color(&self) -> AlphaColor<Srgb> {
276        self.fill_rgbas
277            .first()
278            .map(|&rgba| rgba.into())
279            .unwrap_or(css::WHITE)
280    }
281    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
282        self.fill_rgbas
283            .iter_mut()
284            .for_each(|rgba| *rgba = color.into());
285        self
286    }
287    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
288        self.fill_rgbas.set_opacity(opacity);
289        self
290    }
291}
292
293impl StrokeColor for VItem {
294    fn stroke_color(&self) -> AlphaColor<Srgb> {
295        self.stroke_rgbas
296            .first()
297            .map(|&rgba| rgba.into())
298            .unwrap_or(css::WHITE)
299    }
300    fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
301        self.stroke_rgbas
302            .iter_mut()
303            .for_each(|rgba| *rgba = color.into());
304        self
305    }
306    fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
307        self.stroke_rgbas.set_opacity(opacity);
308        self
309    }
310}
311
312impl StrokeWidth for VItem {
313    fn stroke_width(&self) -> f32 {
314        self.stroke_widths[0].0
315    }
316    fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
317        f(self.stroke_widths.as_mut());
318        self
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use ranim_core::{
325        core_item::vitem::vitem_normal_from_points,
326        traits::{Empty, Interpolatable, RotateTransform},
327    };
328
329    use super::{VItem, geometry::Square};
330
331    #[test]
332    fn generated_vitems_derive_their_interpolated_plane() {
333        let source = VItem::from(Square::new(4.0));
334        assert!(source.normal.is_none());
335        assert!(VItem::empty().normal.is_none());
336
337        let mut target = source.clone();
338        target.rotate_on_y(std::f64::consts::PI / 6.0);
339        target.rotate_on_x(std::f64::consts::PI / 6.0);
340
341        let interpolated = source.lerp(&target, 0.4);
342        assert!(interpolated.normal.is_none());
343
344        let core_item = ranim_core::core_item::vitem::VItem::from(interpolated);
345        let normal = vitem_normal_from_points(&core_item.points);
346        let origin = core_item.points[0].truncate();
347        for point in &core_item.points {
348            let distance = (point.truncate() - origin).dot(normal).abs();
349            assert!(distance < 1e-5, "point is {distance} away from its plane");
350        }
351    }
352}