use crate::time::TimeBase;
#[derive(Clone, Debug)]
pub struct VectorFrame {
pub width: f32,
pub height: f32,
pub view_box: Option<ViewBox>,
pub root: Group,
pub pts: Option<i64>,
pub time_base: TimeBase,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ViewBox {
pub min_x: f32,
pub min_y: f32,
pub width: f32,
pub height: f32,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Node {
Path(PathNode),
Group(Group),
Image(ImageRef),
SoftMask {
mask: Box<Node>,
mask_kind: MaskKind,
content: Box<Node>,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MaskKind {
#[default]
Luminance,
Alpha,
}
#[derive(Clone, Debug)]
pub struct Group {
pub transform: Transform2D,
pub opacity: f32,
pub clip: Option<Path>,
pub children: Vec<Node>,
pub cache_key: Option<u64>,
}
impl Default for Group {
fn default() -> Self {
Self {
transform: Transform2D::identity(),
opacity: 1.0,
clip: None,
children: Vec::new(),
cache_key: None,
}
}
}
#[derive(Clone, Debug)]
pub struct PathNode {
pub path: Path,
pub fill: Option<Paint>,
pub stroke: Option<Stroke>,
pub fill_rule: FillRule,
}
#[derive(Clone, Debug, Default)]
pub struct Path {
pub commands: Vec<PathCommand>,
}
impl Path {
pub fn new() -> Self {
Self::default()
}
pub fn move_to(&mut self, p: Point) -> &mut Self {
self.commands.push(PathCommand::MoveTo(p));
self
}
pub fn line_to(&mut self, p: Point) -> &mut Self {
self.commands.push(PathCommand::LineTo(p));
self
}
pub fn quad_to(&mut self, control: Point, end: Point) -> &mut Self {
self.commands
.push(PathCommand::QuadCurveTo { control, end });
self
}
pub fn cubic_to(&mut self, c1: Point, c2: Point, end: Point) -> &mut Self {
self.commands
.push(PathCommand::CubicCurveTo { c1, c2, end });
self
}
pub fn close(&mut self) -> &mut Self {
self.commands.push(PathCommand::Close);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum PathCommand {
MoveTo(Point),
LineTo(Point),
QuadCurveTo {
control: Point,
end: Point,
},
CubicCurveTo {
c1: Point,
c2: Point,
end: Point,
},
ArcTo {
rx: f32,
ry: f32,
x_axis_rot: f32,
large_arc: bool,
sweep: bool,
end: Point,
},
Close,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Paint {
Solid(Rgba),
LinearGradient(LinearGradient),
RadialGradient(RadialGradient),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Rgba {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Rgba {
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
}
#[derive(Clone, Debug)]
pub struct LinearGradient {
pub start: Point,
pub end: Point,
pub stops: Vec<GradientStop>,
pub spread: SpreadMethod,
}
#[derive(Clone, Debug)]
pub struct RadialGradient {
pub center: Point,
pub radius: f32,
pub focal: Option<Point>,
pub stops: Vec<GradientStop>,
pub spread: SpreadMethod,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GradientStop {
pub offset: f32,
pub color: Rgba,
}
impl GradientStop {
pub const fn new(offset: f32, color: Rgba) -> Self {
Self { offset, color }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SpreadMethod {
#[default]
Pad,
Reflect,
Repeat,
}
#[derive(Clone, Debug)]
pub struct Stroke {
pub width: f32,
pub paint: Paint,
pub cap: LineCap,
pub join: LineJoin,
pub miter_limit: f32,
pub dash: Option<DashPattern>,
}
impl Stroke {
pub fn solid(width: f32, color: Rgba) -> Self {
Self {
width,
paint: Paint::Solid(color),
cap: LineCap::Butt,
join: LineJoin::Miter,
miter_limit: 4.0,
dash: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Clone, Debug, Default)]
pub struct DashPattern {
pub array: Vec<f32>,
pub offset: f32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FillRule {
#[default]
NonZero,
EvenOdd,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transform2D {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub e: f32,
pub f: f32,
}
impl Transform2D {
pub const fn identity() -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
}
}
pub const fn translate(tx: f32, ty: f32) -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: tx,
f: ty,
}
}
pub const fn scale(sx: f32, sy: f32) -> Self {
Self {
a: sx,
b: 0.0,
c: 0.0,
d: sy,
e: 0.0,
f: 0.0,
}
}
pub fn rotate(angle_radians: f32) -> Self {
let (s, c) = angle_radians.sin_cos();
Self {
a: c,
b: s,
c: -s,
d: c,
e: 0.0,
f: 0.0,
}
}
pub fn skew_x(angle_radians: f32) -> Self {
Self {
a: 1.0,
b: 0.0,
c: angle_radians.tan(),
d: 1.0,
e: 0.0,
f: 0.0,
}
}
pub fn skew_y(angle_radians: f32) -> Self {
Self {
a: 1.0,
b: angle_radians.tan(),
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
}
}
pub fn compose(&self, other: &Self) -> Self {
Self {
a: self.a * other.a + self.c * other.b,
b: self.b * other.a + self.d * other.b,
c: self.a * other.c + self.c * other.d,
d: self.b * other.c + self.d * other.d,
e: self.a * other.e + self.c * other.f + self.e,
f: self.b * other.e + self.d * other.f + self.f,
}
}
pub fn apply(&self, p: Point) -> Point {
Point {
x: self.a * p.x + self.c * p.y + self.e,
y: self.b * p.x + self.d * p.y + self.f,
}
}
pub fn is_identity(&self) -> bool {
*self == Self::identity()
}
}
impl Default for Transform2D {
fn default() -> Self {
Self::identity()
}
}
#[derive(Clone, Debug)]
pub struct ImageRef {
pub frame: Box<crate::VideoFrame>,
pub bounds: Rect,
pub transform: Transform2D,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::TimeBase;
fn approx_point(a: Point, b: Point) -> bool {
(a.x - b.x).abs() < 1e-5 && (a.y - b.y).abs() < 1e-5
}
#[test]
fn path_builder_produces_command_sequence() {
let mut p = Path::new();
p.move_to(Point::new(0.0, 0.0))
.line_to(Point::new(10.0, 0.0))
.quad_to(Point::new(15.0, 5.0), Point::new(10.0, 10.0))
.cubic_to(
Point::new(5.0, 15.0),
Point::new(0.0, 10.0),
Point::new(0.0, 0.0),
)
.close();
assert_eq!(p.commands.len(), 5);
assert_eq!(p.commands[0], PathCommand::MoveTo(Point::new(0.0, 0.0)));
assert_eq!(p.commands[4], PathCommand::Close);
}
#[test]
fn transform_identity_round_trips() {
let id = Transform2D::identity();
assert!(id.is_identity());
let p = Point::new(3.5, -2.25);
assert_eq!(id.apply(p), p);
}
#[test]
fn transform_translate_round_trip() {
let t = Transform2D::translate(10.0, -5.0);
assert_eq!(t.apply(Point::new(0.0, 0.0)), Point::new(10.0, -5.0));
assert_eq!(t.apply(Point::new(1.0, 1.0)), Point::new(11.0, -4.0));
}
#[test]
fn transform_scale_round_trip() {
let s = Transform2D::scale(2.0, 3.0);
assert_eq!(s.apply(Point::new(1.0, 1.0)), Point::new(2.0, 3.0));
assert_eq!(s.apply(Point::new(0.0, 0.0)), Point::new(0.0, 0.0));
}
#[test]
fn transform_rotate_quarter_turn() {
let r = Transform2D::rotate(std::f32::consts::FRAC_PI_2);
assert!(approx_point(
r.apply(Point::new(1.0, 0.0)),
Point::new(0.0, 1.0)
));
assert!(approx_point(
r.apply(Point::new(0.0, 1.0)),
Point::new(-1.0, 0.0)
));
}
#[test]
fn transform_compose_identity_is_left_and_right_unit() {
let t = Transform2D::translate(7.0, 11.0);
let id = Transform2D::identity();
assert_eq!(id.compose(&t), t);
assert_eq!(t.compose(&id), t);
}
#[test]
fn transform_compose_translate_then_scale() {
let scale = Transform2D::scale(10.0, 10.0);
let translate = Transform2D::translate(2.0, 3.0);
let composed = scale.compose(&translate);
let result = composed.apply(Point::new(1.0, 1.0));
assert!(approx_point(result, Point::new(30.0, 40.0)));
}
#[test]
fn transform_compose_matches_sequential_apply() {
let a = Transform2D::rotate(0.5);
let b = Transform2D::translate(3.0, -1.0);
let composed = a.compose(&b);
let p = Point::new(2.0, 5.0);
let direct = composed.apply(p);
let stepwise = a.apply(b.apply(p));
assert!(approx_point(direct, stepwise));
}
#[test]
fn group_default_is_identity_opacity_one_no_clip() {
let g = Group::default();
assert!(g.transform.is_identity());
assert_eq!(g.opacity, 1.0);
assert!(g.clip.is_none());
assert!(g.children.is_empty());
}
#[test]
fn group_nesting_with_transforms() {
let inner = Group {
transform: Transform2D::scale(2.0, 2.0),
children: vec![Node::Path(PathNode {
path: {
let mut p = Path::new();
p.move_to(Point::new(1.0, 1.0));
p
},
fill: Some(Paint::Solid(Rgba::opaque(255, 0, 0))),
stroke: None,
fill_rule: FillRule::NonZero,
})],
..Group::default()
};
let outer = Group {
transform: Transform2D::translate(10.0, 10.0),
children: vec![Node::Group(inner)],
..Group::default()
};
match &outer.children[0] {
Node::Group(g) => {
assert_eq!(g.transform, Transform2D::scale(2.0, 2.0));
assert_eq!(g.children.len(), 1);
}
_ => panic!("expected a Group child"),
}
assert_eq!(outer.transform, Transform2D::translate(10.0, 10.0));
}
#[test]
fn vector_frame_construction() {
let frame = VectorFrame {
width: 100.0,
height: 50.0,
view_box: Some(ViewBox {
min_x: 0.0,
min_y: 0.0,
width: 100.0,
height: 50.0,
}),
root: Group::default(),
pts: Some(0),
time_base: TimeBase::new(1, 1000),
};
assert_eq!(frame.width, 100.0);
assert_eq!(frame.height, 50.0);
assert!(frame.view_box.is_some());
assert_eq!(frame.pts, Some(0));
}
#[test]
fn rgba_constructors() {
let c = Rgba::opaque(10, 20, 30);
assert_eq!(c.a, 255);
let c2 = Rgba::new(10, 20, 30, 128);
assert_eq!(c2.a, 128);
}
#[test]
fn gradient_stop_round_trips() {
let s = GradientStop::new(0.5, Rgba::opaque(255, 0, 0));
assert_eq!(s.offset, 0.5);
let s2 = GradientStop::new(0.5, Rgba::opaque(255, 0, 0));
assert_eq!(s, s2);
}
#[test]
fn stroke_solid_defaults() {
let s = Stroke::solid(2.0, Rgba::opaque(0, 0, 0));
assert_eq!(s.width, 2.0);
assert_eq!(s.cap, LineCap::Butt);
assert_eq!(s.join, LineJoin::Miter);
assert_eq!(s.miter_limit, 4.0);
assert!(s.dash.is_none());
}
#[test]
fn soft_mask_construction_and_inspection() {
fn rect_path() -> PathNode {
let mut p = Path::new();
p.move_to(Point::new(0.0, 0.0))
.line_to(Point::new(10.0, 0.0))
.line_to(Point::new(10.0, 10.0))
.line_to(Point::new(0.0, 10.0))
.close();
PathNode {
path: p,
fill: Some(Paint::Solid(Rgba::opaque(255, 255, 255))),
stroke: None,
fill_rule: FillRule::NonZero,
}
}
let n = Node::SoftMask {
mask: Box::new(Node::Path(rect_path())),
mask_kind: MaskKind::Luminance,
content: Box::new(Node::Path(rect_path())),
};
match &n {
Node::SoftMask {
mask_kind, content, ..
} => {
assert_eq!(*mask_kind, MaskKind::Luminance);
match content.as_ref() {
Node::Path(_) => {}
_ => panic!("expected Path content"),
}
}
_ => panic!("expected SoftMask"),
}
}
#[test]
fn mask_kind_default_is_luminance() {
assert_eq!(MaskKind::default(), MaskKind::Luminance);
}
}