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,
}
impl VectorFrame {
pub fn new(width: f32, height: f32) -> Self {
Self {
width,
height,
view_box: None,
root: Group::default(),
pts: None,
time_base: TimeBase::new(1, 1),
}
}
pub fn with_view_box(mut self, view_box: ViewBox) -> Self {
self.view_box = Some(view_box);
self
}
pub fn with_root(mut self, root: Group) -> Self {
self.root = root;
self
}
pub fn with_pts(mut self, pts: i64) -> Self {
self.pts = Some(pts);
self
}
pub fn with_time_base(mut self, time_base: TimeBase) -> Self {
self.time_base = time_base;
self
}
}
impl Default for VectorFrame {
fn default() -> Self {
Self::new(0.0, 0.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ViewBox {
pub min_x: f32,
pub min_y: f32,
pub width: f32,
pub height: f32,
}
impl ViewBox {
pub const fn new(min_x: f32, min_y: f32, width: f32, height: f32) -> Self {
Self {
min_x,
min_y,
width,
height,
}
}
}
#[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,
}
}
}
impl Group {
pub fn new() -> Self {
Self::default()
}
pub fn with_transform(mut self, transform: Transform2D) -> Self {
self.transform = transform;
self
}
pub fn with_opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity;
self
}
pub fn with_clip(mut self, clip: Path) -> Self {
self.clip = Some(clip);
self
}
pub fn with_child(mut self, child: Node) -> Self {
self.children.push(child);
self
}
pub fn with_children(mut self, children: Vec<Node>) -> Self {
self.children = children;
self
}
pub fn with_cache_key(mut self, key: u64) -> Self {
self.cache_key = Some(key);
self
}
}
#[derive(Clone, Debug)]
pub struct PathNode {
pub path: Path,
pub fill: Option<Paint>,
pub stroke: Option<Stroke>,
pub fill_rule: FillRule,
}
impl PathNode {
pub fn new(path: Path) -> Self {
Self {
path,
fill: None,
stroke: None,
fill_rule: FillRule::NonZero,
}
}
pub fn with_fill(mut self, fill: Paint) -> Self {
self.fill = Some(fill);
self
}
pub fn with_stroke(mut self, stroke: Stroke) -> Self {
self.stroke = Some(stroke);
self
}
pub fn with_fill_rule(mut self, fill_rule: FillRule) -> Self {
self.fill_rule = fill_rule;
self
}
}
#[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 }
}
}
impl From<[f32; 2]> for Point {
fn from([x, y]: [f32; 2]) -> Self {
Self { x, y }
}
}
impl From<(f32, f32)> for Point {
fn from((x, y): (f32, 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 }
}
}
impl From<(u8, u8, u8, u8)> for Rgba {
fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self {
Self { r, g, b, a }
}
}
impl From<(u8, u8, u8)> for Rgba {
fn from((r, g, b): (u8, u8, u8)) -> Self {
Self { r, g, b, a: 255 }
}
}
impl From<[u8; 4]> for Rgba {
fn from([r, g, b, a]: [u8; 4]) -> Self {
Self { r, g, b, a }
}
}
impl From<Rgba> for Paint {
fn from(color: Rgba) -> Self {
Paint::Solid(color)
}
}
#[derive(Clone, Debug)]
pub struct LinearGradient {
pub start: Point,
pub end: Point,
pub stops: Vec<GradientStop>,
pub spread: SpreadMethod,
}
impl LinearGradient {
pub fn new(start: Point, end: Point) -> Self {
Self {
start,
end,
stops: Vec::new(),
spread: SpreadMethod::Pad,
}
}
pub fn with_stops(mut self, stops: Vec<GradientStop>) -> Self {
self.stops = stops;
self
}
pub fn with_stop(mut self, stop: GradientStop) -> Self {
self.stops.push(stop);
self
}
pub fn with_spread(mut self, spread: SpreadMethod) -> Self {
self.spread = spread;
self
}
}
#[derive(Clone, Debug)]
pub struct RadialGradient {
pub center: Point,
pub radius: f32,
pub focal: Option<Point>,
pub stops: Vec<GradientStop>,
pub spread: SpreadMethod,
}
impl RadialGradient {
pub fn new(center: Point, radius: f32) -> Self {
Self {
center,
radius,
focal: None,
stops: Vec::new(),
spread: SpreadMethod::Pad,
}
}
pub fn with_focal(mut self, focal: Point) -> Self {
self.focal = Some(focal);
self
}
pub fn with_stops(mut self, stops: Vec<GradientStop>) -> Self {
self.stops = stops;
self
}
pub fn with_stop(mut self, stop: GradientStop) -> Self {
self.stops.push(stop);
self
}
pub fn with_spread(mut self, spread: SpreadMethod) -> Self {
self.spread = spread;
self
}
}
#[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,
}
}
pub fn new(width: f32, paint: Paint) -> Self {
Self {
width,
paint,
cap: LineCap::Butt,
join: LineJoin::Miter,
miter_limit: 4.0,
dash: None,
}
}
pub fn with_paint(mut self, paint: Paint) -> Self {
self.paint = paint;
self
}
pub fn with_cap(mut self, cap: LineCap) -> Self {
self.cap = cap;
self
}
pub fn with_join(mut self, join: LineJoin) -> Self {
self.join = join;
self
}
pub fn with_miter_limit(mut self, miter_limit: f32) -> Self {
self.miter_limit = miter_limit;
self
}
pub fn with_dash(mut self, dash: DashPattern) -> Self {
self.dash = Some(dash);
self
}
}
#[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,
}
impl DashPattern {
pub fn new(array: Vec<f32>) -> Self {
Self { array, offset: 0.0 }
}
pub fn with_offset(mut self, offset: f32) -> Self {
self.offset = offset;
self
}
}
#[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,
}
impl Rect {
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
}
#[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);
}
#[test]
fn vector_frame_default_is_empty_zero_size() {
let f = VectorFrame::default();
assert_eq!(f.width, 0.0);
assert_eq!(f.height, 0.0);
assert!(f.view_box.is_none());
assert!(f.root.children.is_empty());
assert!(f.pts.is_none());
assert_eq!(f.time_base, TimeBase::new(1, 1));
}
#[test]
fn vector_frame_new_sets_canvas_size() {
let f = VectorFrame::new(640.0, 480.0);
assert_eq!(f.width, 640.0);
assert_eq!(f.height, 480.0);
assert!(f.view_box.is_none());
assert!(f.root.children.is_empty());
assert!(f.pts.is_none());
}
#[test]
fn vector_frame_builder_chain() {
let vb = ViewBox::new(0.0, 0.0, 100.0, 100.0);
let f = VectorFrame::new(100.0, 100.0)
.with_view_box(vb)
.with_pts(42)
.with_time_base(TimeBase::new(1, 90_000));
assert_eq!(f.view_box, Some(vb));
assert_eq!(f.pts, Some(42));
assert_eq!(f.time_base, TimeBase::new(1, 90_000));
}
#[test]
fn vector_frame_with_root_replaces_root() {
let root = Group::new().with_opacity(0.5);
let f = VectorFrame::new(10.0, 10.0).with_root(root);
assert_eq!(f.root.opacity, 0.5);
}
#[test]
fn view_box_new_round_trips_fields() {
let vb = ViewBox::new(1.0, 2.0, 3.0, 4.0);
assert_eq!(vb.min_x, 1.0);
assert_eq!(vb.min_y, 2.0);
assert_eq!(vb.width, 3.0);
assert_eq!(vb.height, 4.0);
}
#[test]
fn rect_new_round_trips_fields() {
let r = Rect::new(1.0, 2.0, 3.0, 4.0);
assert_eq!(r.x, 1.0);
assert_eq!(r.y, 2.0);
assert_eq!(r.width, 3.0);
assert_eq!(r.height, 4.0);
}
#[test]
fn group_new_matches_default() {
let a = Group::new();
let b = Group::default();
assert!(a.transform.is_identity());
assert_eq!(a.opacity, b.opacity);
assert!(a.clip.is_none());
assert_eq!(a.children.len(), b.children.len());
assert_eq!(a.cache_key, b.cache_key);
}
#[test]
fn group_builder_chain() {
let mut clip = Path::new();
clip.move_to(Point::new(0.0, 0.0))
.line_to(Point::new(1.0, 1.0))
.close();
let g = Group::new()
.with_transform(Transform2D::translate(5.0, 7.0))
.with_opacity(0.25)
.with_clip(clip)
.with_cache_key(0xdead_beef);
assert_eq!(g.transform, Transform2D::translate(5.0, 7.0));
assert_eq!(g.opacity, 0.25);
assert!(g.clip.is_some());
assert_eq!(g.cache_key, Some(0xdead_beef));
}
#[test]
fn group_with_child_appends() {
let g = Group::new()
.with_child(Node::Group(Group::new()))
.with_child(Node::Group(Group::new().with_opacity(0.5)));
assert_eq!(g.children.len(), 2);
match &g.children[1] {
Node::Group(inner) => assert_eq!(inner.opacity, 0.5),
_ => panic!("expected Group child"),
}
}
#[test]
fn group_with_children_replaces_list() {
let g = Group::new()
.with_child(Node::Group(Group::new()))
.with_children(vec![Node::Group(Group::new().with_opacity(0.1))]);
assert_eq!(g.children.len(), 1);
match &g.children[0] {
Node::Group(inner) => assert_eq!(inner.opacity, 0.1),
_ => panic!("expected Group child"),
}
}
#[test]
fn path_node_new_then_builder() {
let mut p = Path::new();
p.move_to(Point::new(0.0, 0.0))
.line_to(Point::new(10.0, 0.0));
let n = PathNode::new(p)
.with_fill(Paint::Solid(Rgba::opaque(255, 0, 0)))
.with_stroke(Stroke::solid(1.0, Rgba::opaque(0, 0, 0)))
.with_fill_rule(FillRule::EvenOdd);
assert!(n.fill.is_some());
assert!(n.stroke.is_some());
assert_eq!(n.fill_rule, FillRule::EvenOdd);
}
#[test]
fn path_node_new_defaults() {
let n = PathNode::new(Path::new());
assert!(n.fill.is_none());
assert!(n.stroke.is_none());
assert_eq!(n.fill_rule, FillRule::NonZero);
}
#[test]
fn point_from_array_and_tuple() {
let p1: Point = [1.0_f32, 2.0_f32].into();
let p2: Point = (3.0_f32, 4.0_f32).into();
assert_eq!(p1, Point::new(1.0, 2.0));
assert_eq!(p2, Point::new(3.0, 4.0));
}
#[test]
fn rgba_from_tuples_and_array() {
let a: Rgba = (10u8, 20u8, 30u8, 40u8).into();
let b: Rgba = (50u8, 60u8, 70u8).into();
let c: Rgba = [1u8, 2u8, 3u8, 4u8].into();
assert_eq!(a, Rgba::new(10, 20, 30, 40));
assert_eq!(b, Rgba::opaque(50, 60, 70));
assert_eq!(c, Rgba::new(1, 2, 3, 4));
}
#[test]
fn paint_from_rgba_wraps_solid() {
let p: Paint = Rgba::opaque(1, 2, 3).into();
match p {
Paint::Solid(c) => assert_eq!(c, Rgba::opaque(1, 2, 3)),
_ => panic!("expected Paint::Solid"),
}
}
#[test]
fn linear_gradient_new_then_builder() {
let g = LinearGradient::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0))
.with_stop(GradientStop::new(0.0, Rgba::opaque(0, 0, 0)))
.with_stop(GradientStop::new(1.0, Rgba::opaque(255, 255, 255)))
.with_spread(SpreadMethod::Reflect);
assert_eq!(g.start, Point::new(0.0, 0.0));
assert_eq!(g.end, Point::new(1.0, 0.0));
assert_eq!(g.stops.len(), 2);
assert_eq!(g.spread, SpreadMethod::Reflect);
}
#[test]
fn linear_gradient_with_stops_replaces() {
let g = LinearGradient::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0))
.with_stop(GradientStop::new(0.5, Rgba::opaque(0, 0, 0)))
.with_stops(vec![GradientStop::new(0.0, Rgba::opaque(1, 1, 1))]);
assert_eq!(g.stops.len(), 1);
assert_eq!(g.stops[0].offset, 0.0);
}
#[test]
fn radial_gradient_new_then_builder() {
let g = RadialGradient::new(Point::new(5.0, 5.0), 10.0)
.with_focal(Point::new(4.0, 4.0))
.with_stop(GradientStop::new(0.0, Rgba::opaque(0, 0, 0)))
.with_spread(SpreadMethod::Repeat);
assert_eq!(g.center, Point::new(5.0, 5.0));
assert_eq!(g.radius, 10.0);
assert_eq!(g.focal, Some(Point::new(4.0, 4.0)));
assert_eq!(g.stops.len(), 1);
assert_eq!(g.spread, SpreadMethod::Repeat);
}
#[test]
fn radial_gradient_with_stops_replaces() {
let g = RadialGradient::new(Point::new(0.0, 0.0), 1.0)
.with_stop(GradientStop::new(0.5, Rgba::opaque(0, 0, 0)))
.with_stops(vec![GradientStop::new(1.0, Rgba::opaque(1, 1, 1))]);
assert_eq!(g.stops.len(), 1);
assert_eq!(g.stops[0].offset, 1.0);
}
#[test]
fn stroke_new_defaults() {
let s = Stroke::new(3.0, Paint::Solid(Rgba::opaque(0, 0, 0)));
assert_eq!(s.width, 3.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 stroke_builder_chain() {
let s = Stroke::solid(1.0, Rgba::opaque(0, 0, 0))
.with_cap(LineCap::Round)
.with_join(LineJoin::Bevel)
.with_miter_limit(10.0)
.with_dash(DashPattern::new(vec![2.0, 1.0]).with_offset(0.5))
.with_paint(Paint::Solid(Rgba::opaque(128, 128, 128)));
assert_eq!(s.cap, LineCap::Round);
assert_eq!(s.join, LineJoin::Bevel);
assert_eq!(s.miter_limit, 10.0);
let d = s.dash.expect("dash set");
assert_eq!(d.array, vec![2.0, 1.0]);
assert_eq!(d.offset, 0.5);
match s.paint {
Paint::Solid(c) => assert_eq!(c, Rgba::opaque(128, 128, 128)),
_ => panic!("expected Paint::Solid"),
}
}
#[test]
fn dash_pattern_new_zero_offset() {
let d = DashPattern::new(vec![1.0, 2.0, 3.0]);
assert_eq!(d.array, vec![1.0, 2.0, 3.0]);
assert_eq!(d.offset, 0.0);
}
#[test]
fn dash_pattern_with_offset_sets_phase() {
let d = DashPattern::new(vec![1.0]).with_offset(0.25);
assert_eq!(d.offset, 0.25);
}
}