From 48ab099646aec93c850377cc9efc543ee81844aa Mon Sep 17 00:00:00 2001 From: dzil123 <5725958+dzil123@users.noreply.github.com> Date: Thu, 29 Jul 2021 08:11:57 -0700 Subject: [PATCH] SVG path_flat_color renderer --- nannou/Cargo.toml | 1 + nannou/src/draw/renderer/mod.rs | 3 +- nannou/src/draw/renderer/svg/encode.rs | 140 +++++++++++++++++++++++++ nannou/src/draw/renderer/svg/mod.rs | 132 +++++++++++++++++++++++ 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 nannou/src/draw/renderer/svg/encode.rs create mode 100644 nannou/src/draw/renderer/svg/mod.rs diff --git a/nannou/Cargo.toml b/nannou/Cargo.toml index ba4c441f9..447082028 100644 --- a/nannou/Cargo.toml +++ b/nannou/Cargo.toml @@ -28,6 +28,7 @@ rusttype = { version = "0.8", features = ["gpu_cache"] } serde = "1" serde_derive = "1" serde_json = "1" +svg = "0.10" toml = "0.5" walkdir = "2" wgpu_upstream = { version = "0.9", package = "wgpu" } diff --git a/nannou/src/draw/renderer/mod.rs b/nannou/src/draw/renderer/mod.rs index 2dc21e8ce..e858d6d7a 100644 --- a/nannou/src/draw/renderer/mod.rs +++ b/nannou/src/draw/renderer/mod.rs @@ -1,4 +1,5 @@ pub mod primitives; +pub mod svg; pub use self::primitives::{PrimitiveRenderer, RenderContext, RenderPrimitive}; use crate::draw; @@ -524,7 +525,7 @@ impl Renderer { // Collect all draw commands to avoid borrow errors. let draw_cmds: Vec<_> = draw.drain_commands().collect(); - let draw_state = draw.state.borrow_mut(); + let draw_state = draw.state.borrow(); let intermediary_state = draw_state.intermediary_state.borrow(); for cmd in draw_cmds { match cmd { diff --git a/nannou/src/draw/renderer/svg/encode.rs b/nannou/src/draw/renderer/svg/encode.rs new file mode 100644 index 000000000..ae22c7f17 --- /dev/null +++ b/nannou/src/draw/renderer/svg/encode.rs @@ -0,0 +1,140 @@ +use crate::draw::primitive::path::Options; +use crate::draw::properties::LinSrgba; +use crate::lyon::math::Point; +use lyon::lyon_tessellation::{LineCap, LineJoin}; +use lyon::path::{Event, FillRule, PathEvent}; +use nannou_core::color::{Alpha, Srgb}; +use nannou_core::geom::Rect; +use svg::node::element::path::Position::Absolute; +use svg::node::element::path::{Command, Data, Parameters}; +use svg::node::element::{Group, Path}; +use svg::{Document, Node}; + +pub fn svg_document(dims: Rect, elements: Group, background: Option) -> Document { + let dims = dims.absolute(); + let corner = dims.bottom_left(); // smallest x and y + let size = dims.wh(); + let y_offset = dims.h().copysign(dims.y()); + + let mut document = Document::new() + .set("viewBox", (corner.x, corner.y, size.x, size.y)) + .add(elements.set( + "transform", + format!("translate(0 {}) scale(1 -1)", y_offset), // in svg, +y is down; in nannou, +y is up + )); + + if let Some(color) = background { + document.assign( + "style", + format!("background-color: {};", convert_color(color)), + ); + } + + document +} + +pub fn render_path(svg: &mut Group, events: I, color: LinSrgba, options: Options) +where + I: Iterator, +{ + let path_data = lyon_to_svg_path_data(events); + render_path_data(svg, path_data, color, options); +} + +pub fn render_path_data(svg: &mut Group, path_data: Data, color: LinSrgba, options: Options) { + svg.append(path_options(color, options).set("d", path_data)); +} + +pub fn lyon_to_svg_path_data(events: I) -> Data +where + I: Iterator, +{ + let mut data = Vec::with_capacity(events.size_hint().0); + + // follows lyon::FromPolyline convention that the previous Line.to == current Line.from + for event in events { + match event { + Event::Begin { at } => data.push(Command::Move(Absolute, param(at))), + Event::Line { to, .. } => data.push(Command::Line(Absolute, param(to))), + Event::Quadratic { ctrl, to, .. } => { + data.push(Command::QuadraticCurve(Absolute, params(&[ctrl, to]))) + } + Event::Cubic { + ctrl1, ctrl2, to, .. + } => data.push(Command::CubicCurve(Absolute, params(&[ctrl1, ctrl2, to]))), + Event::End { first, close, .. } => { + if close { + data.push(Command::Line(Absolute, param(first))); + } + data.push(Command::Close); + } + } + } + + data.into() +} + +fn path_options(color: LinSrgba, options: Options) -> Path { + match options { + Options::Fill(options) => Path::new() + .set("stroke", "none") + .set("fill", convert_color(color)) + .set( + "fill-rule", + match options.fill_rule { + FillRule::EvenOdd => "evenodd", + FillRule::NonZero => "nonzero", + }, + ), + Options::Stroke(options) => { + if options.start_cap != options.end_cap { + unimplemented!(); + } + Path::new() + .set("fill", "none") + .set("stroke", convert_color(color)) + .set( + "stroke-linecap", + match options.start_cap { + LineCap::Butt => "butt", + LineCap::Square => "round", + LineCap::Round => "square", + }, + ) + .set( + "stroke-line_join", + match options.line_join { + LineJoin::Miter => "miter", + LineJoin::MiterClip => "miter-clip", + LineJoin::Round => "round", + LineJoin::Bevel => "bevel", + }, + ) + .set("stroke-width", options.line_width) + .set("stroke-miterlimit", options.miter_limit) + } + } +} + +fn convert_color(color: LinSrgba) -> String { + let color: Alpha, f32> = color.into_encoding().into_format(); + + format!( + "rgba({}, {}, {}, {:.3})", + color.red, color.green, color.blue, color.alpha + ) +} + +fn param(pt: Point) -> Parameters { + let pt: (f32, f32) = pt.into(); + pt.into() +} + +fn params(points: &[Point]) -> Parameters { + let mut params = Vec::with_capacity(points.len() * 2); + for pt in points { + params.push(pt.x); + params.push(pt.y); + } + params.into() +} diff --git a/nannou/src/draw/renderer/svg/mod.rs b/nannou/src/draw/renderer/svg/mod.rs new file mode 100644 index 000000000..3b371486a --- /dev/null +++ b/nannou/src/draw/renderer/svg/mod.rs @@ -0,0 +1,132 @@ +mod encode; + +use crate::draw::mesh::vertex::TexCoords; +use crate::draw::primitive::path::Options; +use crate::draw::properties::LinSrgba; +use crate::draw::renderer::{PrimitiveRenderer, RenderContext, RenderPrimitive}; +use crate::draw::{self, DrawCommand}; +use crate::glam::{Mat4, Vec2}; +use crate::Draw; +use nannou_core::geom::Rect; +use std::fs::File; +use std::io::{self, Write}; +use std::path::Path; +use svg::node::element::Group; +use svg::Document; + +pub fn render_and_save(dims: Rect, draw: &Draw, path: impl AsRef) { + let document = render(dims, draw); + write_file(path, &document).unwrap(); +} + +fn write_file(path: impl AsRef, document: &Document) -> io::Result<()> { + let mut file = File::create(path)?; + file.write_all(&document.to_string().into_bytes())?; + file.sync_all() +} + +pub fn render(dims: Rect, draw: &Draw) -> Document { + let draw_commands = draw.drain_commands(); + let draw_state = draw.state.borrow(); + let intermediary_state = draw_state.intermediary_state.borrow(); + + let mut svg = Group::new(); + let mut curr_ctxt = Default::default(); + + for command in draw_commands { + match command { + DrawCommand::Context(new_context) => { + if new_context != Default::default() { + unimplemented!(); + } + curr_ctxt = new_context; + } + DrawCommand::Primitive(primitive) => { + let ctxt = RenderContext { + path_event_buffer: &intermediary_state.path_event_buffer, + path_points_colored_buffer: &intermediary_state.path_points_colored_buffer, + path_points_textured_buffer: &intermediary_state.path_points_textured_buffer, + text_buffer: &intermediary_state.text_buffer, + theme: &draw_state.theme, + }; + + let renderer = SvgPrimitiveRenderer { + transform: &curr_ctxt.transform, + theme: &draw_state.theme, + svg: &mut svg, + }; + + let _ = primitive.render_primitive(ctxt, renderer); + } + } + } + + encode::svg_document(dims, svg, draw_state.background_color) +} + +struct SvgPrimitiveRenderer<'a> { + transform: &'a Mat4, + theme: &'a draw::Theme, + svg: &'a mut Group, +} + +impl<'a> PrimitiveRenderer for SvgPrimitiveRenderer<'a> { + fn path_flat_color( + &mut self, + local_transform: Mat4, + events: impl Iterator, + color: Option, + theme_primitive: draw::theme::Primitive, + options: Options, + ) { + let transform = *self.transform * local_transform; + if transform != Mat4::IDENTITY { + unimplemented!(); + } + + let color = self.theme.resolve_color(color, theme_primitive, &options); + encode::render_path(self.svg, events, color, options); + } + + fn path_colored_points( + &mut self, + _local_transform: Mat4, + _points_colored: impl Iterator, + _close: bool, + _options: Options, + ) { + unimplemented!(); + } + + fn path_textured_points( + &mut self, + _local_transform: Mat4, + _points_textured: impl Iterator, + _close: bool, + _options: Options, + ) { + unimplemented!(); + } + + fn mesh( + &mut self, + _local_transform: Mat4, + _vertex_range: std::ops::Range, + _index_range: std::ops::Range, + _fill_color: Option, + ) { + unimplemented!(); + } + + fn text( + &mut self, + local_transform: Mat4, + _text: crate::text::Text, + _color: LinSrgba, + _glyph_colors: Vec, + ) { + let _transform = *self.transform * local_transform; + // TODO: render each glyph as a path_flat_color + unimplemented!(); + } +}