summaryrefslogtreecommitdiff
path: root/tvg/src/render.rs
blob: f8c15a1d363acea0f5e8f32fa307501bc4a9d3d8 (plain)
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!(),
		}
	}
}