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<Vector> 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<Self> {
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<Self> {
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<Self> {
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<u8> for CommandName {
type Error = TvgError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
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<u8> for StyleKind {
type Error = TvgError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
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<Self> {
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<Self> {
Ok(Self::FlatColored {
color_index: read_varuint(reader)?,
})
}
fn read_linear_gradient(reader: &mut impl Read, header: &TvgHeader) -> io::Result<Self> {
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<Self> {
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<C: Color + Clone>(
&self,
color_table: &ColorTable<C>,
point: Point,
) -> Option<C> {
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<C: Color>(
&self,
color_table: &ColorTable<C>,
point: Point,
) -> Option<C> {
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<C: Color + Clone>(
&self,
color_table: &ColorTable<C>,
point: Point,
) -> Option<C> {
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<Self, TvgError> {
// 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<Self, TvgError> {
// 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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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<Self, TvgError> {
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,
})
}
}
|