Skip to main content

oxideav_scene/
object.rs

1//! Scene objects — what's on the canvas, and where.
2
3use std::sync::Arc;
4
5use crate::animation::Animation;
6use crate::duration::Lifetime;
7use crate::id::ObjectId;
8
9/// Pixel format alias; re-exports [`oxideav_core::PixelFormat`] so
10/// callers don't need a direct core dependency just to build a
11/// canvas.
12pub use oxideav_core::PixelFormat;
13
14/// Canvas — either pixel-based (NLE, compositor) or vector-coord
15/// (PDF pages).
16#[non_exhaustive]
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub enum Canvas {
19    /// Pixel raster. Used by the streaming compositor and the NLE
20    /// timeline.
21    Raster {
22        width: u32,
23        height: u32,
24        pixel_format: PixelFormat,
25    },
26    /// Unit-agnostic vector canvas. PDF pages use this; the unit is
27    /// whatever the producer declared. All scene coordinates live in
28    /// this unit; rasterisation happens at export time.
29    Vector {
30        width: f32,
31        height: f32,
32        unit: LengthUnit,
33    },
34}
35
36impl Canvas {
37    /// Convenience for the common case: 8-bit 4:2:0 raster.
38    pub const fn raster(width: u32, height: u32) -> Self {
39        Canvas::Raster {
40            width,
41            height,
42            pixel_format: PixelFormat::Yuv420P,
43        }
44    }
45
46    /// Pixel dims for raster canvases, `None` for vector canvases.
47    pub fn raster_size(&self) -> Option<(u32, u32)> {
48        match self {
49            Canvas::Raster { width, height, .. } => Some((*width, *height)),
50            Canvas::Vector { .. } => None,
51        }
52    }
53}
54
55/// Length unit for vector canvases.
56#[non_exhaustive]
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub enum LengthUnit {
59    /// PostScript / PDF point: 1/72 inch.
60    #[default]
61    Point,
62    /// Millimetre.
63    Millimetre,
64    /// Inch.
65    Inch,
66    /// CSS pixel (96/in).
67    CssPixel,
68    /// Device pixel — what it is is device-dependent.
69    DevicePixel,
70}
71
72/// One renderable element on a scene.
73#[derive(Clone, Debug)]
74pub struct SceneObject {
75    pub id: ObjectId,
76    pub kind: ObjectKind,
77    pub transform: Transform,
78    pub lifetime: Lifetime,
79    pub animations: Vec<Animation>,
80    pub z_order: i32,
81    pub opacity: f32,
82    pub blend_mode: BlendMode,
83    pub effects: Vec<Effect>,
84    pub clip: Option<ClipRect>,
85}
86
87impl Default for SceneObject {
88    fn default() -> Self {
89        SceneObject {
90            id: ObjectId::default(),
91            kind: ObjectKind::Shape(Shape::rect(0.0, 0.0)),
92            transform: Transform::identity(),
93            lifetime: Lifetime::default(),
94            animations: Vec::new(),
95            z_order: 0,
96            opacity: 1.0,
97            blend_mode: BlendMode::default(),
98            effects: Vec::new(),
99            clip: None,
100        }
101    }
102}
103
104/// What a scene object IS.
105#[non_exhaustive]
106#[derive(Clone, Debug)]
107pub enum ObjectKind {
108    Image(ImageSource),
109    Video(VideoSource),
110    Text(TextRun),
111    Shape(Shape),
112    Group(Vec<ObjectId>),
113    Live(LiveStreamHandle),
114}
115
116/// Affine placement on the canvas. Applied in this order:
117/// translate → anchor-relative rotate → scale → skew.
118#[derive(Clone, Copy, Debug, PartialEq)]
119pub struct Transform {
120    pub position: (f32, f32),
121    pub scale: (f32, f32),
122    /// Radians, counter-clockwise, around `anchor`.
123    pub rotation: f32,
124    /// Pivot point in normalised object-local coordinates (0..=1).
125    /// `(0.5, 0.5)` is the object centre.
126    pub anchor: (f32, f32),
127    /// Shear in radians, per axis.
128    pub skew: (f32, f32),
129}
130
131impl Transform {
132    pub const fn identity() -> Self {
133        Transform {
134            position: (0.0, 0.0),
135            scale: (1.0, 1.0),
136            rotation: 0.0,
137            anchor: (0.5, 0.5),
138            skew: (0.0, 0.0),
139        }
140    }
141}
142
143impl Default for Transform {
144    fn default() -> Self {
145        Transform::identity()
146    }
147}
148
149/// Compositing blend — painter's algorithm default is `Normal`.
150#[non_exhaustive]
151#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
152pub enum BlendMode {
153    #[default]
154    Normal,
155    Multiply,
156    Screen,
157    Overlay,
158    Add,
159    /// Subtract destination from source.
160    Subtract,
161    /// Source replaces destination even in transparent regions —
162    /// useful for mask objects.
163    Copy,
164}
165
166/// Filter applied to the object's raster output before compositing.
167/// The parameter map is opaque here; per-effect implementations in
168/// sibling crates interpret it.
169#[derive(Clone, Debug)]
170pub struct Effect {
171    pub name: String,
172    pub params: Vec<(String, f32)>,
173}
174
175/// Axis-aligned clipping rectangle in canvas coordinates.
176#[derive(Clone, Copy, Debug, PartialEq)]
177pub struct ClipRect {
178    pub x: f32,
179    pub y: f32,
180    pub width: f32,
181    pub height: f32,
182}
183
184/// Bitmap source. Either an owned frame, a shared frame handle, or
185/// a path that the renderer resolves on first use.
186#[non_exhaustive]
187#[derive(Clone, Debug)]
188pub enum ImageSource {
189    /// Fully-decoded frame, `Arc`-shared so cloning is cheap.
190    Decoded(Arc<oxideav_core::VideoFrame>),
191    /// Filesystem path — resolved lazily by the renderer.
192    Path(String),
193    /// Raw bytes of an encoded image file (PNG/JPEG/etc).
194    EncodedBytes(Arc<[u8]>),
195}
196
197/// Video source. Resolves packets via the container layer on
198/// demand; the scene renderer advances it to the requested PTS.
199#[non_exhaustive]
200#[derive(Clone, Debug)]
201pub enum VideoSource {
202    Path(String),
203    EncodedBytes(Arc<[u8]>),
204}
205
206/// Styled text run. Font resolution + shaping land in a separate
207/// crate; this type only carries what the model needs to preserve
208/// (the string itself + structural + appearance metadata).
209#[derive(Clone, Debug, Default)]
210pub struct TextRun {
211    pub text: String,
212    pub font_family: String,
213    pub font_weight: u16,
214    pub font_size: f32,
215    /// `0xRRGGBBAA`.
216    pub color: u32,
217    /// Optional explicit glyph-advance vector (PDF-style). If
218    /// `None`, the rasteriser shapes on the fly.
219    pub advances: Option<Vec<f32>>,
220    pub italic: bool,
221    pub underline: bool,
222}
223
224/// Vector shape primitive.
225#[non_exhaustive]
226#[derive(Clone, Debug)]
227pub enum Shape {
228    Rect {
229        width: f32,
230        height: f32,
231        fill: u32,
232        stroke: Option<Stroke>,
233        corner_radius: f32,
234    },
235    Polygon {
236        points: Vec<(f32, f32)>,
237        fill: u32,
238        stroke: Option<Stroke>,
239    },
240    Path {
241        /// SVG path data ("M10,10 L20,20 …").
242        data: String,
243        fill: u32,
244        stroke: Option<Stroke>,
245    },
246}
247
248impl Shape {
249    /// Zero-size placeholder rect with no fill. Used by
250    /// `SceneObject::default`.
251    pub const fn rect(width: f32, height: f32) -> Self {
252        Shape::Rect {
253            width,
254            height,
255            fill: 0,
256            stroke: None,
257            corner_radius: 0.0,
258        }
259    }
260}
261
262#[derive(Clone, Copy, Debug, PartialEq)]
263pub struct Stroke {
264    pub color: u32,
265    pub width: f32,
266}
267
268/// Opaque handle to a live input feed. The renderer polls it for
269/// the most recent frame at render time.
270#[derive(Clone, Debug)]
271pub struct LiveStreamHandle {
272    /// Implementation-defined URI — `rtmp://…`, `file://named-pipe`,
273    /// etc. The streaming compositor resolves this against a
274    /// pluggable `LiveSource` registry (pending crate).
275    pub uri: String,
276    /// Optional hint for the expected frame size. The renderer will
277    /// fall back to the actual frame size if it differs.
278    pub hint_size: Option<(u32, u32)>,
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn raster_canvas_size() {
287        let c = Canvas::raster(640, 480);
288        assert_eq!(c.raster_size(), Some((640, 480)));
289    }
290
291    #[test]
292    fn vector_canvas_no_raster_size() {
293        let c = Canvas::Vector {
294            width: 595.0,
295            height: 842.0,
296            unit: LengthUnit::Point,
297        };
298        assert!(c.raster_size().is_none());
299    }
300
301    #[test]
302    fn transform_identity_roundtrip() {
303        let t = Transform::identity();
304        assert_eq!(t.position, (0.0, 0.0));
305        assert_eq!(t.scale, (1.0, 1.0));
306        assert_eq!(t.anchor, (0.5, 0.5));
307    }
308
309    #[test]
310    fn scene_object_default_is_neutral() {
311        let o = SceneObject::default();
312        assert_eq!(o.opacity, 1.0);
313        assert_eq!(o.blend_mode, BlendMode::Normal);
314        assert!(o.animations.is_empty());
315    }
316}