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