use crate::{
colors::{self, blend, lerp, Color, ColorTable},
commands::{Command, Point, Rectangle, Style, Vector},
header::TvgHeader,
path::{Instruction, InstructionData, Path, Sweep},
TvgFile,
};
fn distance(p0: Point, p1: Point) -> f64 {
(p0 - p1).magnitude().abs()
}
fn rotation_matrix(angle: f64) -> [[f64; 2]; 2] {
let s = angle.sin();
let c = angle.cos();
[[c, -s], [s, c]]
}
fn apply_matrix(matrix: [[f64; 2]; 2], vector: Vector) -> Vector {
Vector {
x: vector.x * matrix[0][0] + vector.y * matrix[0][1],
y: vector.x * matrix[1][0] + vector.y * matrix[1][1],
}
}
fn apply_matrix_point(matrix: [[f64; 2]; 2], point: Point) -> Point {
Point {
x: point.x * matrix[0][0] + point.y * matrix[0][1],
y: point.x * matrix[1][0] + point.y * matrix[1][1],
}
}
fn lerp_f64(a: f64, b: f64, x: f64) -> f64 {
a + (b - a) * x
}
fn lerp_and_reduce(vals: &[f64], f: f64) -> Vec<f64> {
let mut result = Vec::with_capacity(vals.len() - 1);
for i in 0..(vals.len() - 1) {
result.push(lerp_f64(vals[i], vals[i + 1], f));
}
result
}
fn lerp_and_reduce_to_one(vals: &[f64], f: f64) -> f64 {
if vals.len() == 1 {
vals[0]
} else {
lerp_and_reduce_to_one(&lerp_and_reduce(vals, f), f)
}
}
/// A version of [`Point`] that uses usizes for a PixMap
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UPoint {
x: usize,
y: usize,
}
impl UPoint {
fn new(x: usize, y: usize) -> UPoint {
Self { x, y }
}
}
struct FrameBuffer<C: Color> {
width: usize,
height: usize,
pixels: Box<[C]>,
scale_x: f64,
scale_y: f64,
}
impl<C: Color + Clone + Default> FrameBuffer<C> {
fn new(width: usize, height: usize, scale_x: f64, scale_y: f64) -> Self {
let pixel_count = width * height;
Self {
width,
height,
pixels: vec![C::default(); pixel_count].into_boxed_slice(),
scale_x,
scale_y,
}
}
fn point_to_upoint(&self, point: Point) -> UPoint {
todo!()
}
fn upoint_to_point(&self, upoint: UPoint) -> Point {
todo!()
}
/// Blends the new color with the given pixel. Returns `None` if the destination is invalid
fn set_pixel(&mut self, x: usize, y: usize, color: C) -> Option<()> {
if x >= self.width || y >= self.height {
return None;
}
let offset = y * self.width + x;
let destination_pixel = self.pixels.get_mut(offset)?;
blend(destination_pixel, &color);
Some(())
}
fn draw_line(
&mut self,
color_table: ColorTable<C>,
style: Style,
width_start: f64,
width_end: f64,
line_start: Point,
line_end: Point,
) -> Option<()> {
let mut min_x = usize::MAX;
let mut min_y = usize::MAX;
let mut max_x = usize::MIN;
let mut max_y = usize::MIN;
let max_width = f64::max(width_start, width_end);
let points = [line_start, line_end];
for pt in points {
min_x = usize::min(min_x, (self.scale_x * (pt.x - max_width)).floor() as usize);
min_y = usize::min(min_y, (self.scale_y * (pt.y - max_width)).floor() as usize);
max_x = usize::max(max_x, (self.scale_x * (pt.x - max_width)).floor() as usize);
max_y = usize::max(max_y, (self.scale_y * (pt.y - max_width)).floor() as usize);
}
// limit to framebuffer size
min_x = usize::min(min_x, self.width);
min_y = usize::min(min_y, self.height);
max_x = usize::min(max_x, self.width);
max_y = usize::min(max_y, self.height);
for y in min_y..=max_y {
for x in min_x..=max_x {
let point = self.upoint_to_point(UPoint { x, y });
let dist = todo!() as f64;
if dist >= 0.0 {
self.set_pixel(x, y, style.get_color_at(&color_table, point)?);
}
}
}
Some(())
}
}
#[derive(Debug, Default)]
struct PathMaker {
points: Vec<Point>,
width: Vec<f64>,
}
impl PathMaker {
fn new(width: f64, start: Point) -> Self {
let points = vec![start];
let width = vec![width];
Self { points, width }
}
fn render_line(&mut self, width: f64, to_point: Point) {
self.points.push(to_point);
self.width.push(width);
}
fn render_horizontal_line(&mut self, width: f64, x: f64) {
self.points.push(Point {
x,
y: self.points.last().unwrap().y,
});
self.width.push(width);
}
fn render_vertical_line(&mut self, width: f64, y: f64) {
self.points.push(Point {
x: self.points.last().unwrap().x,
y,
});
self.width.push(width);
}
fn render_cubic_bezier(
&mut self,
control_0: Point,
control_1: Point,
point: Point,
start_width: f64,
end_width: f64,
) {
const BEZIER_POLY_COUNT: usize = 16;
let previous = self.points.last().unwrap();
let oct0x = [previous.x, control_0.x, control_1.x, point.x];
let oct0y = [previous.y, control_0.y, control_1.y, point.y];
for i in 1..BEZIER_POLY_COUNT {
let f = i as f64 / BEZIER_POLY_COUNT as f64;
let x = lerp_and_reduce_to_one(&oct0x, f);
let y = lerp_and_reduce_to_one(&oct0y, f);
self.points.push(Point { x, y });
self.width.push(lerp_f64(start_width, end_width, f));
}
self.points.push(point);
self.width.push(end_width);
}
fn render_quadratic_bezier(
&mut self,
control: Point,
target: Point,
start_width: f64,
end_width: f64,
) {
const BEZIER_POLY_COUNT: usize = 16;
let previous = self.points.last().unwrap();
let oct0x = [previous.x, control.x, target.x];
let oct0y = [previous.y, control.y, target.y];
for i in 1..BEZIER_POLY_COUNT {
let f = i as f64 / BEZIER_POLY_COUNT as f64;
let x = lerp_and_reduce_to_one(&oct0x, f);
let y = lerp_and_reduce_to_one(&oct0y, f);
self.points.push(Point { x, y });
self.width.push(lerp_f64(start_width, end_width, f));
}
self.points.push(target);
self.width.push(end_width);
}
fn render_circle(
&mut self,
p0: Point,
p1: Point,
mut radius: f64,
large_arc: bool,
turn_left: bool,
end_width: f64,
) {
const CIRCLE_POLY_COUNT: usize = 100;
let start_width = *self.width.last().unwrap();
let left_side = turn_left == large_arc;
let delta = (p1 - p0).scale(0.5);
let midpoint = p0 + delta;
let radius_vec = if left_side {
Vector {
x: -delta.y,
y: delta.x,
}
} else {
Vector {
x: delta.y,
y: -delta.x,
}
};
let len_squared = radius_vec.x * radius_vec.x + radius_vec.y * radius_vec.y;
if (len_squared - 0.03 > radius * radius) || radius < 0.0 {
radius = len_squared.sqrt();
}
let to_center = radius_vec.scale(f64::max(0.0, radius * radius / len_squared - 1.0).sqrt());
let center = midpoint + to_center;
let angle = len_squared.sqrt().clamp(-1.0, 1.0).asin() * 2.0;
let arc = if large_arc {
std::f64::consts::TAU - angle
} else {
angle
};
let position = p0 - center;
for i in 0..CIRCLE_POLY_COUNT {
let arc = if turn_left { -arc } else { arc };
let step_matrix = rotation_matrix(i as f64 * arc / CIRCLE_POLY_COUNT as f64);
let point = center + apply_matrix(step_matrix, position);
self.points.push(point);
self.width.push(lerp_f64(
start_width,
end_width,
i as f64 / CIRCLE_POLY_COUNT as f64,
));
}
self.points.push(p1);
}
fn render_ellipse(
&mut self,
p0: Point,
p1: Point,
radius_x: f64,
radius_y: f64,
rotation: f64,
large_arc: bool,
turn_left: bool,
end_width: f64,
) {
let radius_min = distance(p0, p1) / 2.0;
let radius_max = (radius_x * radius_x + radius_y * radius_y).sqrt();
let upscale = if radius_max < radius_min {
radius_min / radius_max
} else {
1.0
};
let ratio = radius_x / radius_y;
let rotation = rotation_matrix(-rotation.to_radians());
let transform = [
[rotation[0][0] / upscale, rotation[0][1] / upscale],
[
rotation[1][0] / upscale * ratio,
rotation[1][1] / upscale * ratio,
],
];
let transform_back = [
[rotation[1][1] * upscale, -rotation[0][1] / ratio * upscale],
[-rotation[1][0] * upscale, rotation[0][0] / ratio * upscale],
];
let mut tmp = PathMaker::default();
tmp.render_circle(
apply_matrix_point(transform, p0),
apply_matrix_point(transform, p1),
radius_x * upscale,
large_arc,
turn_left,
end_width,
);
for i in 0..tmp.points.len() {
let point = tmp.points[i];
self.points.push(apply_matrix_point(transform_back, point));
self.width.push(tmp.width[i]);
}
}
fn close(&mut self, width: f64) {
self.points.push(self.points[0]);
self.width.push(width)
}
}
fn render_path(path: Path, width: f64) {
for segment in path.segments.iter() {
let mut path_maker = PathMaker::new(width, segment.start);
for instruction in segment.instructions.iter() {
let line_width = instruction
.line_width
.unwrap_or(*path_maker.width.last().unwrap());
let data = instruction.data;
match data {
InstructionData::Line { position } => path_maker.render_line(line_width, position),
InstructionData::HorizontalLine { x } => {
path_maker.render_horizontal_line(line_width, x)
}
InstructionData::VerticalLine { y } => {
path_maker.render_vertical_line(line_width, y)
}
InstructionData::CubicBezier {
control_0,
control_1,
point_1,
} => path_maker.render_cubic_bezier(
control_0,
control_1,
point_1,
*path_maker.width.last().unwrap(),
line_width,
),
InstructionData::ArcCircle {
large_arc,
sweep,
radius,
target,
} => path_maker.render_circle(
*path_maker.points.last().unwrap(),
target,
radius,
large_arc,
sweep == Sweep::Left,
line_width,
),
InstructionData::ArcEllipse {
large_arc,
sweep,
radius_x,
radius_y,
rotation,
target,
} => path_maker.render_ellipse(
*path_maker.points.last().unwrap(),
target,
radius_x,
radius_y,
rotation,
large_arc,
sweep == Sweep::Left,
line_width,
),
InstructionData::ClosePath => path_maker.close(line_width),
InstructionData::QuadraticBezier { control, target } => path_maker
.render_quadratic_bezier(
control,
target,
*path_maker.width.last().unwrap(),
line_width,
),
}
}
}
}
pub enum AntiAliasing {
X1 = 1,
X4 = 2,
X9 = 3,
X16 = 4,
X25 = 6,
X49 = 7,
X64 = 8,
}
pub fn render<C: Color + Clone + Default>(
file: &TvgFile<C>,
width: u32,
height: u32,
scale_x: f32,
scale_y: f32,
anti_alias: AntiAliasing,
) {
let mut framebuffer: FrameBuffer<C> = FrameBuffer::new(
width as usize,
height as usize,
scale_x.into(),
scale_y.into(),
);
let header = &file.header;
let color_table = &file.color_table;
for command in file.commands.iter() {
match command {
Command::EndOfDocument => break,
Command::FillPolygon {
fill_style,
polygon,
} => {
todo!()
}
Command::FillRectangles {
fill_style,
rectangles,
} => todo!(),
Command::FillPath { fill_style, path } => todo!(),
Command::DrawLines {
line_style,
line_width,
lines,
} => todo!(),
Command::DrawLineLoop {
line_style,
line_width,
points,
} => todo!(),
Command::DrawLineStrip {
line_style,
line_width,
points,
} => todo!(),
Command::DrawLinePath {
line_style,
line_width,
path,
} => todo!(),
Command::OutlineFillPolygon {
fill_style,
line_style,
line_width,
points,
} => todo!(),
Command::OutlineFillRectangles {
fill_style,
line_style,
line_width,
rectangles,
} => todo!(),
Command::OutlineFillPath {
fill_style,
line_style,
line_width,
path,
} => todo!(),
}
}
}
|