Skip to content

Instantly share code, notes, and snippets.

@emilk
Last active February 9, 2022 08:44
Show Gist options
  • Save emilk/77271e69c50b7bffc018af47835c11af to your computer and use it in GitHub Desktop.
Save emilk/77271e69c50b7bffc018af47835c11af to your computer and use it in GitHub Desktop.
A bad way to show an SVG in egui
//! 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