Last active
February 9, 2022 08:44
-
-
Save emilk/77271e69c50b7bffc018af47835c11af to your computer and use it in GitHub Desktop.
A bad way to show an SVG in egui
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! A not so great way to show an SVG in egui. | |
//! | |
//! This keeps the SVG as vector graphics to make it scalable, | |
//! but it has a few problems: | |
//! | |
//! * There is no anti-aliasing (would require MSAA backend) | |
//! * No support for gradients and text in SVG | |
//! * Has some bugs in it | |
//! * It is slow | |
//! * It is a lot of code | |
//! | |
//! A much better approach can be found in the `eframe/examples/svg.rs` which uses `resvg + usvg + tiny_skia` to | |
//! rasterize the svg into an image, and display it as a texture. | |
//! | |
//! Based on https://github.com/nical/lyon/blob/master/examples/wgpu_svg/src/main.rs | |
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release | |
use eframe::{egui, epaint, epi}; | |
use lyon::math::Point; | |
use lyon::path::PathEvent; | |
use lyon::tessellation::geometry_builder::*; | |
use lyon::tessellation::{self, FillOptions, FillTessellator, StrokeOptions, StrokeTessellator}; | |
use usvg::prelude::*; | |
struct Svg { | |
/// What to paint. | |
mesh: epaint::Mesh, | |
/// Where the visible part of the mesh lies. | |
rect: epaint::Rect, | |
} | |
const FALLBACK_COLOR: usvg::Color = usvg::Color { | |
red: 255, | |
green: 0, | |
blue: 255, | |
}; | |
struct VertexConstructor { | |
transform: usvg::Transform, | |
color: epaint::Color32, | |
} | |
impl FillVertexConstructor<epaint::Vertex> for VertexConstructor { | |
fn new_vertex(&mut self, vertex: tessellation::FillVertex) -> epaint::Vertex { | |
let pos = transform(&self.transform, vertex.position().to_array()); | |
epaint::Vertex { | |
pos, | |
uv: epaint::WHITE_UV, | |
color: self.color, | |
} | |
} | |
} | |
impl StrokeVertexConstructor<epaint::Vertex> for VertexConstructor { | |
fn new_vertex(&mut self, vertex: tessellation::StrokeVertex) -> epaint::Vertex { | |
let pos = transform(&self.transform, vertex.position().to_array()); | |
epaint::Vertex { | |
pos, | |
uv: epaint::WHITE_UV, | |
color: self.color, | |
} | |
} | |
} | |
fn transform(t: &usvg::Transform, [x, y]: [f32; 2]) -> epaint::Pos2 { | |
let (x, y) = t.apply(x as f64, y as f64); | |
epaint::Pos2::new(x as f32, y as f32) | |
} | |
impl Svg { | |
fn load_bytes(svg_data: &[u8]) -> Result<Self, usvg::Error> { | |
// Maximum allowed distance to the path when building an approximation. | |
let tolerance = StrokeOptions::DEFAULT_TOLERANCE; | |
let mut fill_tess = FillTessellator::new(); | |
let mut stroke_tess = StrokeTessellator::new(); | |
let mut lyon_mesh: VertexBuffers<_, u32> = VertexBuffers::new(); | |
let opt = usvg::Options::default(); | |
let rtree = usvg::Tree::from_data(svg_data, &opt)?; | |
let rect = rtree.svg_node().view_box.rect; | |
for node in rtree.root().descendants() { | |
if let usvg::NodeKind::Path(ref p) = *node.borrow() { | |
if let Some(ref fill) = p.fill { | |
// fall back to always use color fill | |
// no gradients (yet?) | |
let color = match fill.paint { | |
usvg::Paint::Color(c) => c, | |
_ => FALLBACK_COLOR, | |
}; | |
let color = to_color32(color, fill.opacity.value() as f32); | |
fill_tess | |
.tessellate( | |
PathConvIter::new(p), | |
&FillOptions::tolerance(tolerance), | |
&mut BuffersBuilder::new( | |
&mut lyon_mesh, | |
VertexConstructor { | |
transform: node.transform(), | |
color, | |
}, | |
), | |
) | |
.expect("Error during tesselation!"); | |
} | |
if let Some(ref stroke) = p.stroke { | |
let (stroke_color, stroke_opts) = convert_stroke(stroke, tolerance); | |
let color = to_color32(stroke_color, stroke.opacity.value() as f32); | |
let _ = stroke_tess.tessellate( | |
PathConvIter::new(p), | |
&stroke_opts.with_tolerance(tolerance), | |
&mut BuffersBuilder::new( | |
&mut lyon_mesh, | |
VertexConstructor { | |
transform: node.transform(), | |
color, | |
}, | |
), | |
); | |
} | |
} | |
} | |
let mesh = epaint::Mesh { | |
vertices: lyon_mesh.vertices, | |
indices: lyon_mesh.indices, | |
texture_id: Default::default(), | |
}; | |
let rect = epaint::Rect::from_min_size( | |
epaint::pos2(rect.left() as f32, rect.top() as f32), | |
epaint::vec2(rect.width() as f32, rect.height() as f32), | |
); | |
Ok(Self { mesh, rect }) | |
} | |
} | |
fn to_color32(color: usvg::Color, alpha: f32) -> egui::Color32 { | |
egui::Color32::from_rgb(color.red, color.green, color.blue).linear_multiply(alpha) | |
} | |
// Some glue between usvg's iterators and lyon's: | |
struct PathConvIter<'a> { | |
iter: std::slice::Iter<'a, usvg::PathSegment>, | |
prev: Point, | |
first: Point, | |
needs_end: bool, | |
deferred: Option<PathEvent>, | |
} | |
impl<'a> PathConvIter<'a> { | |
fn new(p: &'a usvg::Path) -> Self { | |
Self { | |
iter: p.data.iter(), | |
first: Point::new(0.0, 0.0), | |
prev: Point::new(0.0, 0.0), | |
deferred: None, | |
needs_end: false, | |
} | |
} | |
} | |
impl<'l> Iterator for PathConvIter<'l> { | |
type Item = PathEvent; | |
fn next(&mut self) -> Option<PathEvent> { | |
fn point(x: &f64, y: &f64) -> Point { | |
Point::new((*x) as f32, (*y) as f32) | |
} | |
if self.deferred.is_some() { | |
return self.deferred.take(); | |
} | |
let next = self.iter.next(); | |
match next { | |
Some(usvg::PathSegment::MoveTo { x, y }) => { | |
if self.needs_end { | |
let last = self.prev; | |
let first = self.first; | |
self.needs_end = false; | |
self.prev = point(x, y); | |
self.deferred = Some(PathEvent::Begin { at: self.prev }); | |
self.first = self.prev; | |
Some(PathEvent::End { | |
last, | |
first, | |
close: false, | |
}) | |
} else { | |
self.first = point(x, y); | |
self.needs_end = true; | |
Some(PathEvent::Begin { at: self.first }) | |
} | |
} | |
Some(usvg::PathSegment::LineTo { x, y }) => { | |
self.needs_end = true; | |
let from = self.prev; | |
self.prev = point(x, y); | |
Some(PathEvent::Line { | |
from, | |
to: self.prev, | |
}) | |
} | |
Some(usvg::PathSegment::CurveTo { | |
x1, | |
y1, | |
x2, | |
y2, | |
x, | |
y, | |
}) => { | |
self.needs_end = true; | |
let from = self.prev; | |
self.prev = point(x, y); | |
Some(PathEvent::Cubic { | |
from, | |
ctrl1: point(x1, y1), | |
ctrl2: point(x2, y2), | |
to: self.prev, | |
}) | |
} | |
Some(usvg::PathSegment::ClosePath) => { | |
self.needs_end = false; | |
self.prev = self.first; | |
Some(PathEvent::End { | |
last: self.prev, | |
first: self.first, | |
close: true, | |
}) | |
} | |
None => { | |
if self.needs_end { | |
self.needs_end = false; | |
let last = self.prev; | |
let first = self.first; | |
Some(PathEvent::End { | |
last, | |
first, | |
close: false, | |
}) | |
} else { | |
None | |
} | |
} | |
} | |
} | |
} | |
fn convert_stroke(s: &usvg::Stroke, tolerance: f32) -> (usvg::Color, StrokeOptions) { | |
let color = match s.paint { | |
usvg::Paint::Color(c) => c, | |
_ => FALLBACK_COLOR, | |
}; | |
let linecap = match s.linecap { | |
usvg::LineCap::Butt => tessellation::LineCap::Butt, | |
usvg::LineCap::Square => tessellation::LineCap::Square, | |
usvg::LineCap::Round => tessellation::LineCap::Round, | |
}; | |
let linejoin = match s.linejoin { | |
usvg::LineJoin::Miter => tessellation::LineJoin::Miter, | |
usvg::LineJoin::Bevel => tessellation::LineJoin::Bevel, | |
usvg::LineJoin::Round => tessellation::LineJoin::Round, | |
}; | |
let opt = StrokeOptions::tolerance(tolerance) | |
.with_line_width(s.width.value() as f32) | |
.with_line_cap(linecap) | |
.with_line_join(linejoin); | |
(color, opt) | |
} | |
// ---------------------------------------------------------------------------- | |
struct MyApp { | |
svg: Svg, | |
} | |
impl Default for MyApp { | |
fn default() -> Self { | |
Self { | |
svg: Svg::load_bytes(include_bytes!("rustacean-flat-happy.svg")).unwrap(), | |
} | |
} | |
} | |
impl epi::App for MyApp { | |
fn name(&self) -> &str { | |
"svg example" | |
} | |
fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) { | |
egui::CentralPanel::default().show(ctx, |ui| { | |
ui.heading("SVG example"); | |
ui.label(format!( | |
"Tessellated SVG has {} vertices and {} indices", | |
self.svg.mesh.vertices.len(), | |
self.svg.mesh.indices.len(), | |
)); | |
ui.label("Unfortunately you will get jagged edges because the eframe backend doesn't do any multisample anti-aliasing, and the SVG tessellator doesn't do any feathering."); | |
ui.separator(); | |
show_svg(ui, &self.svg, ui.available_size()); | |
}); | |
} | |
} | |
fn show_svg(ui: &mut egui::Ui, svg: &Svg, max_size: epaint::Vec2) -> egui::Response { | |
// Scale to fit the max_size: | |
let mut desired_size = svg.rect.size(); | |
desired_size *= (max_size.x / desired_size.x).min(1.0); | |
desired_size *= (max_size.y / desired_size.y).min(1.0); | |
// Figure out where to put it: | |
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::hover()); | |
let scale = rect.width() / svg.rect.width(); | |
// Scale and translate the mesh: | |
let transform_vertex = |v: &epaint::Vertex| { | |
let pos = epaint::Pos2 { | |
x: (v.pos.x - svg.rect.left()) * scale + rect.left(), | |
y: (v.pos.y - svg.rect.top()) * scale + rect.top(), | |
}; | |
epaint::Vertex { | |
pos, | |
uv: v.uv, | |
color: v.color, | |
} | |
}; | |
let mesh = epaint::Mesh { | |
texture_id: svg.mesh.texture_id, | |
indices: svg.mesh.indices.clone(), | |
vertices: svg.mesh.vertices.iter().map(transform_vertex).collect(), | |
}; | |
ui.painter().add(epaint::Shape::mesh(mesh)); | |
response | |
} | |
fn main() { | |
let options = eframe::NativeOptions::default(); | |
eframe::run_native(Box::new(MyApp::default()), options); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment