From f8a80039c74332e2101a177ef3fde31ef2077224 Mon Sep 17 00:00:00 2001 From: Micha White Date: Thu, 15 Aug 2024 20:14:15 -0400 Subject: Lots a changes --- tvg/src/render.rs | 506 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 tvg/src/render.rs (limited to 'tvg/src/render.rs') 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 { + 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 { + width: usize, + height: usize, + pixels: Box<[C]>, + scale_x: f64, + scale_y: f64, +} + +impl FrameBuffer { + 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, + 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, + width: Vec, +} + +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( + file: &TvgFile, + width: u32, + height: u32, + scale_x: f32, + scale_y: f32, + anti_alias: AntiAliasing, +) { + let mut framebuffer: FrameBuffer = 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!(), + } + } +} -- cgit v1.2.3