use std::io::{self, Read}; use std::ops::{Add, Mul, Sub}; use byteorder::ReadBytesExt; use raise::yeet; use crate::{ colors::{self, Color, ColorTable}, header::TvgHeader, path::Path, read_unit, read_varuint, Decode, TvgError, }; /// A X and Y coordinate pair. #[derive(Debug, Clone, Copy)] pub struct Vector { /// Horizontal distance of the point to the origin. pub x: f64, /// Vertical distance of the point to the origin. pub y: f64, } impl Vector { pub fn magnitude(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() } pub fn scale(&self, scale: f64) -> Vector { Vector { x: self.x * scale, y: self.y * scale, } } } impl Mul for Vector { type Output = f64; fn mul(self, rhs: Self) -> Self::Output { self.x * rhs.x + self.y * rhs.y } } /// A X and Y coordinate pair. #[derive(Debug, Clone, Copy)] pub struct Point { /// Horizontal distance of the point to the origin. pub x: f64, /// Vertical distance of the point to the origin. pub y: f64, } impl Sub for Point { type Output = Vector; fn sub(self, rhs: Self) -> Self::Output { Vector { x: self.x - rhs.x, y: self.y - rhs.y, } } } impl Add for Point { type Output = Self; fn add(self, vector: Vector) -> Self::Output { Self { x: self.x + vector.x, y: self.y + vector.y, } } } impl Decode for Point { fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { Ok(Self { x: read_unit(reader, header)?, y: read_unit(reader, header)?, }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct Rectangle { /// Horizontal distance of the left side to the origin. pub x: f64, /// Vertical distance of the upper side to the origin. pub y: f64, /// Horizontal extent of the rectangle. pub width: f64, /// Vertical extent of the rectangle. pub height: f64, } impl Decode for Rectangle { fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { let x = 5; Ok(Self { x: read_unit(reader, header)?, y: read_unit(reader, header)?, width: read_unit(reader, header)?, height: read_unit(reader, header)?, }) } } #[derive(Debug, Clone, Copy)] pub struct Line { /// Start point of the line. start: Point, /// End point of the line. end: Point, } impl Decode for Line { fn read(reader: &mut impl Read, header: &TvgHeader) -> io::Result { Ok(Self { start: Point::read(reader, header)?, end: Point::read(reader, header)?, }) } } /// A command that can be encoded. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u8)] enum CommandName { /// Determines end of file. EndOfDocument = 0, /// Fills an N-gon. FillPolygon = 1, /// Fills a set of [`Rectangle`]s. FillRectangles = 2, /// Fills a free-form [`Path`]. FillPath = 3, /// Draws a set of lines. DrawLines = 4, /// Draws the outline of a polygon. DrawLineLoop = 5, /// Draws a list of end-to-end lines. DrawLineStrip = 6, /// Draws a free-form [`Path`]. DrawLinePath = 7, /// Draws a filled polygon with an outline. OutlineFillPolygon = 8, /// Draws several filled [`Rectangle`]s with an outline. OutlineFillRectangles = 9, /// This command combines the [`FillPath`] and [`DrawLinePath`] command /// into one. OutlineFillPath = 10, } impl TryFrom for CommandName { type Error = TvgError; fn try_from(value: u8) -> Result { match value { 0 => Ok(Self::EndOfDocument), 1 => Ok(Self::FillPolygon), 2 => Ok(Self::FillRectangles), 3 => Ok(Self::FillPath), 4 => Ok(Self::DrawLines), 5 => Ok(Self::DrawLineLoop), 6 => Ok(Self::DrawLineStrip), 7 => Ok(Self::DrawLinePath), 8 => Ok(Self::OutlineFillPolygon), 9 => Ok(Self::OutlineFillRectangles), 10 => Ok(Self::OutlineFillPath), v => Err(TvgError::InvalidCommand(v)), } } } /// The type of style the command uses as a primary style. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(u8)] pub enum StyleKind { /// The shape is uniformly colored with a single color. FlatColored = 0, /// The shape is colored with a linear gradient. LinearGradient = 1, /// The shape is colored with a radial gradient. RadialGradient = 2, } impl TryFrom for StyleKind { type Error = TvgError; fn try_from(value: u8) -> Result { match value { 0 => Ok(Self::FlatColored), 1 => Ok(Self::LinearGradient), 2 => Ok(Self::RadialGradient), _ => Err(TvgError::InvalidStyleKind), } } } /// The style kind, along with the colors and points used to render the style. #[derive(Debug, Clone, Copy)] pub enum Style { /// The shape is uniformly colored with the color at `color_index` in the /// [`ColorTable`]. FlatColored { /// The index in the [`ColorTable`]. color_index: u32, }, /// The gradient is formed by a mental line between `point_0` and /// `point_1`. /// /// The color at `point_0` is the color at `color_index_0` in the color /// table. The color at `point_1` is the color at `color_index_1` in the /// [`ColorTable`]. On the line, the color is interpolated between the two /// points. Each point that is not on the line is orthogonally projected to /// the line and the color at that point is sampled. Points that are not /// projectable onto the line have either the color at `point_0` if they /// are closed to `point_0` or vice versa for `point_1`. LinearGradient { /// The start point of the gradient. point_0: Point, /// The end point of the gradient. point_1: Point, /// The color at [`point_0`]. color_index_0: u32, /// The color at [`point_1`]. color_index_1: u32, }, /// The gradient is formed by a mental circle with the center at `point_0` /// and `point_1` being somewhere on the circle outline. Thus, the radius /// of said circle is the distance between `point_0` and `point_1`. /// /// The color at `point_0` is the color at `color_index_0` in the color /// table. The color on the circle outline is the color at `color_index_1` /// in the [`ColorTable`]. If a sampled point is inside the circle, the /// color is interpolated based on the distance to the center and the /// radius. If the point is not in the circle itself, the color at /// `color_index_1` is always taken. RadialGradient { /// The center point of the mental circle. point_0: Point, /// The end point of the gradient. point_1: Point, /// The color at `point_0`. color_index_0: u32, /// The color at `point_1`. color_index_1: u32, }, } impl Style { fn read(reader: &mut impl Read, header: &TvgHeader, kind: StyleKind) -> io::Result { match kind { StyleKind::FlatColored => Self::read_flat_colored(reader), StyleKind::LinearGradient => Self::read_linear_gradient(reader, header), StyleKind::RadialGradient => Self::read_radial_gradient(reader, header), } } fn read_flat_colored(reader: &mut impl Read) -> io::Result { Ok(Self::FlatColored { color_index: read_varuint(reader)?, }) } fn read_linear_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result { Ok(Self::LinearGradient { point_0: Point::read(reader, header)?, point_1: Point::read(reader, header)?, color_index_0: read_varuint(reader)?, color_index_1: read_varuint(reader)?, }) } fn read_radial_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result { Ok(Self::RadialGradient { point_0: Point::read(reader, header)?, point_1: Point::read(reader, header)?, color_index_0: read_varuint(reader)?, color_index_1: read_varuint(reader)?, }) } fn linear_get_color_at( &self, color_table: &ColorTable, point: Point, ) -> Option { let Self::LinearGradient { point_0, point_1, color_index_0, color_index_1 } = self else { panic!() }; // TODO remove these unwraps let color_0 = color_table.get(*color_index_0 as usize)?; let color_1 = color_table.get(*color_index_1 as usize)?; let direction = *point_1 - *point_0; let delta = point - *point_0; if direction * delta <= 0.0 { return Some(color_0.clone()); } else if direction * (point - *point_1) >= 0.0 { return Some(color_1.clone()); } let gradient_length = direction.magnitude(); let proj = (direction * delta) / (direction * direction); let gradient_position = direction.scale(proj).magnitude(); let f = gradient_position / gradient_length; Some(colors::lerp(color_0, color_1, f)) } fn radial_get_color_at( &self, color_table: &ColorTable, point: Point, ) -> Option { let Self::RadialGradient { point_0, point_1, color_index_0, color_index_1 } = self else { panic!() }; // TODO remove these unwraps let color_0 = color_table.get(*color_index_0 as usize)?; let color_1 = color_table.get(*color_index_1 as usize)?; let distance_max = (*point_1 - *point_0).magnitude(); let distance_is = (point - *point_0).magnitude(); let f = distance_is / distance_max; Some(colors::lerp(color_0, color_1, f)) } pub fn get_color_at( &self, color_table: &ColorTable, point: Point, ) -> Option { match self { Self::FlatColored { color_index } => color_table.get(*color_index as usize).cloned(), Self::LinearGradient { .. } => self.linear_get_color_at(color_table, point), Self::RadialGradient { .. } => self.radial_get_color_at(color_table, point), } } } /// TinyVG files contain a sequence of draw commands that must be executed in /// the defined order to get the final result. Each draw command adds a new 2D /// primitive to the graphic. #[derive(Debug, Clone)] pub enum Command { /// If this command is read, the TinyVG file has ended. /// /// This command must have prim_style_kind to be set to 0, so the last byte /// of every TinyVG file is `0x00`. Every byte after this command is /// considered not part of the TinyVG data and can be used for other /// purposes like metadata or similar. EndOfDocument, /// Fills a polygon with N [`Point`]s. /// /// The number of points must be at least 3. Files that encode a lower /// value must be discarded as ”invalid” by a conforming implementation. /// /// The polygon specified in polygon must be drawn using the even-odd rule. /// That means that if for any point to be inside the polygon, a line to /// infinity must cross an odd number of polygon segments. FillPolygon { /// The style that is used to fill the polygon. fill_style: Style, /// The points of the polygon. polygon: Box<[Point]>, }, /// Fills a list of [`Rectangle`]s. /// /// The rectangles must be drawn first to last, which is the order they /// appear in the file. FillRectangles { /// The style that is used to fill all rectangles. fill_style: Style, /// The list of rectangles to be filled. rectangles: Box<[Rectangle]>, }, /// Fills a [`Path`]. /// /// For the filling, all path segments are considered a polygon each (drawn /// with even-odd rule) that, when overlap, also perform the even odd rule. /// This allows the user to carve out parts of the path and create /// arbitrarily shaped surfaces. FillPath { /// The style that is used to fill the path. fill_style: Style, /// A [`Path`] with `segment_count` segments. path: Path, }, /// Draws a set of [`Line`]s. /// /// Each line is `line_width` units wide, and at least a single display /// pixel. This means that line_width of 0 is still visible, even though /// only marginally. This allows very thin outlines. DrawLines { /// The style that is used to draw the all lines. line_style: Style, /// The width of the lines. line_width: f64, /// The list of lines. lines: Box<[Line]>, }, /// Draws a polygon. /// /// Each line is `line_width` units wide. The lines are drawn between /// consecutive points as well as the first and the last point. DrawLineLoop { /// The style that is used to draw the all lines. line_style: Style, /// The width of the line. line_width: f64, /// The points of the polygon. points: Box<[Point]>, }, /// Draws a list of consecutive lines. /// /// The lines are drawn between consecutive points, but contrary to /// [`DrawLineLoop`], the first and the last point are not connected. DrawLineStrip { /// The style that is used to draw the all rectangles. line_style: Style, /// The width of the line. line_width: f64, /// The points of the line strip. points: Box<[Point]>, }, /// Draws a [`Path`]. /// /// The outline of the path is `line_width` units wide. DrawLinePath { /// The style that is used to draw the path. line_style: Style, /// The width of the line. line_width: f64, /// A path with `segment_count` segments. path: Path, }, /// Fills a polygon and draws an outline at the same time. /// /// This command is a combination of [`FillPolygon`] and [`DrawLineLoop`]. /// It first performs a [`FillPolygon`] with the `fill_style`, then /// performs [`DrawLineLoop`] with `line_style` and `line_width`. /// /// The outline commands use a reduced number of elements. The maximum /// number of points is 64. OutlineFillPolygon { /// The style that is used to fill the polygon. fill_style: Style, /// The style that is used to draw the outline of the polygon. line_style: Style, /// The width of the line. line_width: f64, /// The set of points of this polygon. points: Box<[Point]>, }, /// Fills and outlines a list of [`Rectangle`]s. /// /// For each rectangle, it is first filled, then its outline is drawn, then /// the next rectangle is drawn. /// /// The outline commands use a reduced number of elements, the maximum /// number of points is 64. OutlineFillRectangles { /// The style that is used to fill the rectangles. fill_style: Style, /// The style that is used to draw the outline of the rectangles. line_style: Style, /// The width of the line. line_width: f64, /// The list of rectangles to be drawn. rectangles: Box<[Rectangle]>, }, /// Fills a path and draws an outline at the same time. /// /// This command is a combination of [`FillPath`] and [`DrawLinePath`]. It /// first performs a [`FillPath`] with the `fill_style`, then performs /// [`DrawLinePath`] with `line_style` and `line_width`. OutlineFillPath { /// The style that is used to fill the path. fill_style: Style, /// The style that is used to draw the outline of the path. line_style: Style, /// The width of the line. line_width: f64, /// The path that should be drawn. path: Path, }, } /// The header is different for outline commands, so we use this as a helper struct OutlineHeader { count: u32, fill_style: Style, line_style: Style, line_width: f64, } impl OutlineHeader { fn read( reader: &mut impl Read, header: &TvgHeader, prim_style_kind: StyleKind, ) -> Result { // the count and secondary style kind are stores in the same byte let byte = reader.read_u8()?; let count = (byte & 0b0011_1111) as u32 + 1; let sec_style_kind = StyleKind::try_from((byte & 0b1100_0000) >> 6)?; let fill_style = Style::read(reader, header, prim_style_kind)?; let line_style = Style::read(reader, header, sec_style_kind)?; let line_width = read_unit(reader, header)?; Ok(Self { count, fill_style, line_style, line_width, }) } } impl Command { pub fn read(reader: &mut impl Read, header: &TvgHeader) -> Result { // the command name and primary style kind are stores in the same byte let byte = reader.read_u8()?; let command = CommandName::try_from(byte & 0b0011_1111)?; let style_kind = StyleKind::try_from((byte & 0b1100_0000) >> 6)?; match command { CommandName::EndOfDocument => Self::end_of_document(style_kind), CommandName::FillPolygon => Self::read_fill_polygon(reader, header, style_kind), CommandName::FillRectangles => Self::read_fill_rectangles(reader, header, style_kind), CommandName::FillPath => Self::read_fill_path(reader, header, style_kind), CommandName::DrawLines => Self::read_draw_lines(reader, header, style_kind), CommandName::DrawLineLoop => Self::read_draw_line_loop(reader, header, style_kind), CommandName::DrawLineStrip => Self::read_draw_line_strip(reader, header, style_kind), CommandName::DrawLinePath => Self::read_draw_line_path(reader, header, style_kind), CommandName::OutlineFillPolygon => { Self::read_outline_fill_polygon(reader, header, style_kind) } CommandName::OutlineFillRectangles => { Self::read_outline_fill_rectangles(reader, header, style_kind) } CommandName::OutlineFillPath => { Self::read_outline_fill_path(reader, header, style_kind) } } } pub fn is_end_of_document(&self) -> bool { matches!(self, Self::EndOfDocument) } fn read_command_header( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> io::Result<(u32, Style)> { // every command adds one to the count let count = read_varuint(reader)? + 1; let style = Style::read(reader, header, style_kind)?; Ok((count, style)) } fn end_of_document(style_kind: StyleKind) -> Result { if style_kind != StyleKind::FlatColored { Err(TvgError::InvalidEndOfDocument(style_kind as u8)) } else { Ok(Self::EndOfDocument) } } fn read_fill_polygon( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let (point_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; if point_count < 3 { yeet!(TvgError::InvalidPolygon(point_count)); } let polygon = Point::read_multiple(reader, header, point_count)?; Ok(Self::FillPolygon { fill_style, polygon, }) } fn read_fill_rectangles( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let (rectangle_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; let rectangles = Rectangle::read_multiple(reader, header, rectangle_count)?; Ok(Self::FillRectangles { fill_style, rectangles, }) } fn read_fill_path( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let (segment_count, fill_style) = Self::read_command_header(reader, header, style_kind)?; let path = Path::read(reader, header, segment_count)?; Ok(Self::FillPath { fill_style, path }) } fn read_draw_lines( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let (line_count, line_style) = Self::read_command_header(reader, header, style_kind)?; let line_width = read_unit(reader, header)?; let lines = Line::read_multiple(reader, header, line_count)?; Ok(Self::DrawLines { line_style, line_width, lines, }) } fn read_draw_line_loop( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let (point_count, line_style) = Self::read_command_header(reader, header, style_kind)?; let line_width = read_unit(reader, header)?; let points = Point::read_multiple(reader, header, point_count)?; Ok(Self::DrawLineLoop { line_style, line_width, points, }) } fn read_draw_line_strip( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let (point_count, line_style) = Self::read_command_header(reader, header, style_kind)?; let line_width = read_unit(reader, header)?; let points = Point::read_multiple(reader, header, point_count)?; Ok(Self::DrawLineStrip { line_style, line_width, points, }) } fn read_draw_line_path( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let (segment_count, line_style) = Self::read_command_header(reader, header, style_kind)?; let line_width = read_unit(reader, header)?; let path = Path::read(reader, header, segment_count)?; Ok(Self::DrawLinePath { line_style, line_width, path, }) } fn read_outline_fill_polygon( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let OutlineHeader { count: segment_count, fill_style, line_style, line_width, } = OutlineHeader::read(reader, header, style_kind)?; let points = Point::read_multiple(reader, header, segment_count)?; Ok(Self::OutlineFillPolygon { fill_style, line_style, line_width, points, }) } fn read_outline_fill_rectangles( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let OutlineHeader { count: rect_count, fill_style, line_style, line_width, } = OutlineHeader::read(reader, header, style_kind)?; let rectangles = Rectangle::read_multiple(reader, header, rect_count)?; Ok(Self::OutlineFillRectangles { fill_style, line_style, line_width, rectangles, }) } fn read_outline_fill_path( reader: &mut impl Read, header: &TvgHeader, style_kind: StyleKind, ) -> Result { let OutlineHeader { count: segment_count, fill_style, line_style, line_width, } = OutlineHeader::read(reader, header, style_kind)?; let path = Path::read(reader, header, segment_count)?; Ok(Self::OutlineFillPath { fill_style, line_style, line_width, path, }) } }