summaryrefslogtreecommitdiff
path: root/tvg/src/render.rs
diff options
context:
space:
mode:
authorMicha White <botahamec@outlook.com>2024-08-15 20:14:15 -0400
committerMicha White <botahamec@outlook.com>2024-08-15 20:14:15 -0400
commitf8a80039c74332e2101a177ef3fde31ef2077224 (patch)
treef887c96bf9879a28b7ce914ad96161f63ee83190 /tvg/src/render.rs
parent488c7ed94b0662222fa0d825ab81b60b0b1e5d6c (diff)
Lots a changes
Diffstat (limited to 'tvg/src/render.rs')
-rw-r--r--tvg/src/render.rs506
1 files changed, 506 insertions, 0 deletions
diff --git a/tvg/src/render.rs b/tvg/src/render.rs
new file mode 100644
index 0000000..f8c15a1
--- /dev/null
+++ b/tvg/src/render.rs
@@ -0,0 +1,506 @@
+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!(),
+ }
+ }
+}