#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
use std::{collections::VecDeque, time::Instant, error::Error, fs};
use femtovg::{renderer::OpenGl, Renderer, Canvas, Path, Paint, Color};
use winit::{window::Window, event_loop::EventLoop};
#[cfg_attr(coverage_nightly, coverage(off))] fn main() -> Result<(), Box<dyn Error>> {
eprintln!(r"{} v{}-g{}, {}, {} 🦀", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"),
env!("BUILD_GIT_HASH"), env!("BUILD_TIMESTAMP"), env!("CARGO_PKG_AUTHORS"));
println!("Usage: {} [<path-to-file>]", std::env::args().next().unwrap());
let event_loop = EventLoop::new()?;
use winit::event::{Event, WindowEvent, MouseButton, ElementState};
#[cfg(not(target_arch = "wasm32"))]
let (window, surface, glctx,
mut canvas) = create_window(&event_loop, "SVG Renderer - Femtovg")?;
#[cfg(target_arch = "wasm32")] let (window, mut canvas) = {
use winit::platform::web::WindowBuilderExtWebSys;
use wasm_bindgen::JsCast;
let canvas = web_sys::window().unwrap() .document().unwrap().get_element_by_id("canvas").unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>().unwrap();
let window = winit::window::WindowBuilder::new()
.with_canvas(Some(canvas)).build(&event_loop).unwrap();
let canvas = Canvas::new(OpenGl::new_from_html_canvas(&canvas)
.expect("Cannot create renderer")).expect("Cannot create canvas");
(window, canvas) };
#[cfg(feature = "rive-rs")] let mut viewport = rive_rs::Viewport::default();
#[cfg(feature = "rive-rs")] let mut scene = None;
#[cfg(feature = "rive-rs")] use inlottie::rive::NanoVG;
#[cfg(feature = "lottie")] let mut lottie = None;
#[cfg(feature = "lottie")] use inlottie::schema::Animation;
let mut usvg_opts = usvg::Options::default();
usvg_opts.fontdb_mut().load_system_fonts(); let mut tree = None;
let path = std::env::args().nth(1).unwrap_or("data/tiger.svg".to_owned());
match path.rfind('.').map_or("", |i| &path[1 + i..]) {
#[cfg(feature = "lottie")]
"json" => lottie = Animation::from_reader(fs::File::open(&path)?).ok(),
#[cfg(feature = "rive-rs")]
"riv" => scene = NanoVG::new_scene(&fs::read(&path)?),
"svg" => tree = usvg::Tree::from_data(&fs::read(&path)?, &usvg_opts).ok(),
_ => { let size = window.inner_size();
canvas.set_size(size.width, size.height, 1.);
eprintln!("File format is not supported: {path}");
}
}
let (mut dragging, mut focused, mut paused) = (false, true, false);
let (mut perf, mut prevt) = (PerfGraph::new(), Instant::now());
let mut mouse = (0., 0.);
event_loop.run(|event, elwt| {
let mut resize_canvas = |csize: (f32, f32)| {
let wsize = window.inner_size();
let wsize = (wsize.width as f32, wsize.height as f32);
canvas.reset_transform(); mouse = (0., 0.);
let scale = (wsize.0 / csize.0).min(wsize.1 / csize.1) * 0.95;
canvas.translate((wsize.0 - csize.0 * scale) / 2., (wsize.1 - csize.1 * scale) / 2.);
canvas.set_size (wsize.0 as _, wsize.1 as _, 1.); canvas.scale(scale, scale);
};
match event {
Event::WindowEvent { window_id: _, event } => match event {
WindowEvent::CloseRequested | WindowEvent::Destroyed => elwt.exit(),
#[cfg(not(target_arch = "wasm32"))] WindowEvent::Resized(size) => {
surface.resize(&glctx, size.width .try_into().unwrap(),
size.height.try_into().unwrap());
if let Some(tree) = &tree {
resize_canvas((tree.size().width(), tree.size().height()));
}
#[cfg(feature = "lottie")] if let Some(lottie) = &lottie {
resize_canvas((lottie.w as _, lottie.h as _));
}
#[cfg(feature = "rive-rs")] if scene.is_some() { viewport.resize(size.width, size.height);
canvas.set_size(size.width, size.height, 1.);
}
}
WindowEvent::KeyboardInput { event, .. } => {
if event.state == ElementState::Pressed {
use winit::keyboard::{Key, NamedKey};
match event.logical_key.as_ref() {
Key::Named(NamedKey::Space) => {
prevt = Instant::now(); paused = !paused;
}
#[cfg(feature = "lottie")]
Key::Character(char) => if paused { use std::time::Duration;
match char {
"n" | "N" => {
prevt = Instant::now() - Duration::from_millis((1000. /
lottie.as_ref().map_or(60., |lottie|
lottie.fr)) as _); window.request_redraw();
} _ => (),
}
} _ => (),
}
}
}
WindowEvent::MouseInput { button: MouseButton::Left,
state, .. } => match state {
ElementState::Pressed => { dragging = true;
#[cfg(feature = "rive-rs")] if let Some(scene) = &mut scene {
scene.pointer_down(mouse.0, mouse.1, &viewport);
}
}
ElementState::Released => { dragging = false;
#[cfg(feature = "rive-rs")] if let Some(scene) = &mut scene {
scene.pointer_up (mouse.0, mouse.1, &viewport);
}
}
},
WindowEvent::Focused(bl) => focused = bl,
WindowEvent::MouseWheel { device_id: _, delta:
winit::event::MouseScrollDelta::LineDelta(_, y), .. } => {
let pt = canvas.transform().inversed()
.transform_point(mouse.0, mouse.1);
canvas.translate( pt.0, pt.1);
canvas.scale(1. + (y / 10.), 1. + (y / 10.));
canvas.translate(-pt.0, -pt.1);
}
WindowEvent::CursorMoved { device_id: _,
position, .. } => {
if dragging {
let p0 = canvas.transform().inversed()
.transform_point(mouse.0, mouse.1);
let p1 = canvas.transform().inversed()
.transform_point(position.x as _, position.y as _);
canvas.translate(p1.0 - p0.0, p1.1 - p0.1);
} mouse = (position.x as _, position.y as _);
#[cfg(feature = "rive-rs")] if let Some(scene) = &mut scene {
scene.pointer_move(mouse.0, mouse.1, &viewport);
}
}
WindowEvent::DroppedFile(path) => { tree = None;
#[cfg(feature = "lottie")] { lottie = None; }
#[cfg(feature = "rive-rs")] { scene = None; }
let file = fs::read(&path).unwrap_or(vec![]);
match path.extension().and_then(|ext| ext.to_str()) {
Some("svg") => tree = usvg::Tree::from_data(&file,
&usvg_opts).ok().map(|tree| {
resize_canvas((tree.size().width(), tree.size().height())); tree
}),
#[cfg(feature = "rive-rs")]
Some("riv") => scene = NanoVG::new_scene(&file),
#[cfg(feature = "lottie")]
Some("json") => lottie = Animation::from_reader(
fs::File::open(&path).unwrap()).ok().map(|lottie| {
resize_canvas((lottie.w as _, lottie.h as _)); lottie }),
_ => eprintln!("File format is not supported: {}", path.display()),
}
window.request_redraw();
}
WindowEvent::RedrawRequested => {
#[cfg(any(feature = "rive-rs", feature = "lottie"))]
let elapsed = prevt.elapsed(); prevt = Instant::now();
canvas.clear_rect(0, 0, canvas.width(), canvas.height(),
Color::rgbf(0.4, 0.4, 0.4));
#[cfg(feature = "rive-rs")] if let Some(scene) = &mut scene {
if !scene.advance_and_maybe_draw(&mut NanoVG::new(&mut canvas),
elapsed, &mut viewport) { return }
}
#[cfg(feature = "lottie")] if let Some(lottie) = &mut lottie {
if !(lottie.render_next_frame(&mut canvas,
elapsed.as_secs_f32())) { return }
}
if let Some(tree) = &tree {
render_nodes(&mut canvas, &mouse, tree.root(),
&usvg::Transform::identity());
}
perf.render(&mut canvas, 3., 3.); canvas.flush();
perf.update(prevt.elapsed().as_secs_f32());
#[cfg(not(target_arch = "wasm32"))] surface.swap_buffers(&glctx).expect("Could not swap buffers");
}
_ => ()
},
Event::AboutToWait => if focused && !paused { window.request_redraw() },
Event::LoopExiting => elwt.exit(),
_ => () }})?; Ok(()) }
#[cfg(not(target_arch = "wasm32"))]
use glutin::{surface::{Surface, WindowSurface}, context::PossiblyCurrentContext, prelude::*};
#[allow(clippy::type_complexity)] #[cfg(not(target_arch = "wasm32"))]
fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<(Window,
Surface<WindowSurface>, PossiblyCurrentContext, Canvas<OpenGl>), Box<dyn Error>> {
use glutin::{config::ConfigTemplateBuilder, surface::SurfaceAttributesBuilder,
context::{ContextApi, ContextAttributesBuilder}, display::GetGlDisplay};
use {raw_window_handle::HasRawWindowHandle, glutin_winit::DisplayBuilder};
let mut wsize = event_loop.primary_monitor().unwrap().size();
wsize.width /= 2; wsize.height /= 2; use std::num::NonZeroU32;
let (window, gl_config) = DisplayBuilder::new()
.with_window_builder(Some(winit::window::WindowBuilder::new()
.with_inner_size(wsize).with_resizable(true).with_title(title)))
.build(event_loop, ConfigTemplateBuilder::new().with_alpha_size(8),
|configs|
configs.reduce(|config, accum| {
if (config.supports_transparency().unwrap_or(false) &
!accum.supports_transparency().unwrap_or(false)) ||
config.num_samples() < accum.num_samples() { config } else { accum }
}).unwrap())?;
let window = window.unwrap(); let raw_window_handle = window.raw_window_handle();
let gl_display = gl_config.display();
let surf_attr =
SurfaceAttributesBuilder::<WindowSurface>::new()
.build(raw_window_handle, NonZeroU32::new(wsize. width).unwrap(),
NonZeroU32::new(wsize.height).unwrap());
let surface = unsafe {
gl_display.create_window_surface(&gl_config, &surf_attr)? };
let glctx = Some(unsafe {
gl_display.create_context(&gl_config,
&ContextAttributesBuilder::new()
.build(Some(raw_window_handle)))
.unwrap_or_else(|_| gl_display.create_context(&gl_config,
&ContextAttributesBuilder::new()
.with_context_api(ContextApi::Gles(None))
.build(Some(raw_window_handle))).expect("Failed to create context"))
}).take().unwrap().make_current(&surface)?;
let mut canvas = Canvas::new(unsafe {
OpenGl::new_from_function_cstr(|s|
gl_display.get_proc_address(s) as *const _) }?)?;
#[cfg(target_os = "macos")] let _ = canvas.add_font_dir("/Library/fonts");
canvas.add_font_dir("data/fonts").expect("Cannot add font dir/files");
Ok((window, surface, glctx, canvas))
}
pub struct PerfGraph { que: VecDeque<f32>, max: f32, sum: f32 }
impl PerfGraph {
#[allow(clippy::new_without_default)] pub fn new() -> Self {
Self { que: VecDeque::with_capacity(100), max: 0., sum: 0. }
}
pub fn update(&mut self, ft: f32) { let fps = 1. / ft; if self.max < fps { self.max = fps } if self.que.len() == 100 { self.sum -= self.que.pop_front().unwrap_or(0.); }
self.que.push_back(fps); self.sum += fps;
}
pub fn render<T: Renderer>(&self, canvas: &mut Canvas<T>, x: f32, y: f32) {
let (rw, rh, mut path) = (100., 20., Path::new());
let mut paint = Paint::color(Color::rgba(0, 0, 0, 99));
path.rect(0., 0., rw, rh);
canvas.save(); canvas.reset_transform(); canvas.translate(x, y);
canvas.fill_path(&path, &paint);
path = Path::new(); path.move_to(0., rh);
for i in 0..self.que.len() { path.line_to(rw * i as f32 / self.que.len() as f32, rh - rh * self.que[i] / self.max);
} path.line_to(rw, rh); paint.set_color(Color::rgba(255, 192, 0, 128));
canvas.fill_path(&path, &paint);
paint.set_color(Color::rgba(240, 240, 240, 255));
paint.set_text_baseline(femtovg::Baseline::Top);
paint.set_text_align(femtovg::Align::Right);
paint.set_font_size(14.0);
let fps = self.sum / self.que.len() as f32; let _ = canvas.fill_text(rw - 10., 0., &format!("{fps:.2} FPS"), &paint);
canvas.restore();
}
}
fn render_nodes<T: Renderer>(canvas: &mut Canvas<T>, mouse: &(f32, f32),
parent: &usvg::Group, trfm: &usvg::Transform) {
fn convert_paint(paint: &usvg::Paint, opacity: usvg::Opacity,
_trfm: &usvg::Transform) -> Option<Paint> {
fn convert_stops(stops: &[usvg::Stop], opacity: usvg::Opacity) -> Vec<(f32, Color)> {
stops.iter().map(|stop| { let color = stop.color();
let mut fc = Color::rgb(color.red, color.green, color.blue);
fc.set_alphaf((stop.opacity() * opacity).get()); (stop.offset().get(), fc)
}).collect::<Vec<_>>()
}
Some(match paint { usvg::Paint::Pattern(_) => { eprintln!("Not support pattern painting"); return None }
usvg::Paint::Color(color) => {
let mut fc = Color::rgb(color.red, color.green, color.blue);
fc.set_alphaf(opacity.get()); Paint::color(fc)
}
usvg::Paint::LinearGradient(grad) =>
Paint::linear_gradient_stops(grad.x1(), grad.y1(), grad.x2(), grad.y2(),
convert_stops(grad.stops(), opacity)),
usvg::Paint::RadialGradient(grad) => {
Paint::radial_gradient_stops(grad.fx(), grad.fy(), (grad.cx() - grad.fx()).hypot(grad.cy() - grad.fy()),
grad.r().get(), convert_stops(grad.stops(), opacity))
}
})
}
for child in parent.children() { match child {
usvg::Node::Group(group) => render_nodes(canvas, mouse, group, &trfm.pre_concat(group.transform())),
usvg::Node::Path(path) => if path.is_visible() {
let tpath = if trfm.is_identity() { None
} else { path.data().clone().transform(*trfm) }; let mut fpath = Path::new();
for seg in tpath.as_ref().unwrap_or(path.data()).segments() {
use usvg::tiny_skia_path::PathSegment;
match seg { PathSegment::Close => fpath.close(),
PathSegment::MoveTo(pt) => fpath.move_to(pt.x, pt.y),
PathSegment::LineTo(pt) => fpath.line_to(pt.x, pt.y),
PathSegment::QuadTo(ctrl, end) =>
fpath.quad_to (ctrl.x, ctrl.y, end.x, end.y),
PathSegment::CubicTo(ctrl0, ctrl1, end) =>
fpath.bezier_to (ctrl0.x, ctrl0.y, ctrl1.x, ctrl1.y, end.x, end.y),
}
}
use femtovg::{FillRule, LineCap, LineJoin};
let fpaint = path.fill().and_then(|fill|
convert_paint(fill.paint(), fill.opacity(), trfm).map(|mut paint| {
paint.set_fill_rule(match fill.rule() {
usvg::FillRule::NonZero => FillRule::NonZero,
usvg::FillRule::EvenOdd => FillRule::EvenOdd,
}); paint
})
);
let lpaint = path.stroke().and_then(|stroke|
convert_paint(stroke.paint(), stroke.opacity(), trfm).map(|mut paint| {
paint.set_miter_limit(stroke.miterlimit().get());
paint.set_line_width (stroke.width().get());
paint.set_line_join(match stroke.linejoin() { usvg::LineJoin::MiterClip |
usvg::LineJoin::Miter => LineJoin::Miter,
usvg::LineJoin::Round => LineJoin::Round,
usvg::LineJoin::Bevel => LineJoin::Bevel,
});
paint.set_line_cap (match stroke.linecap () {
usvg::LineCap::Butt => LineCap::Butt,
usvg::LineCap::Round => LineCap::Round,
usvg::LineCap::Square => LineCap::Square,
}); paint
})
);
match path.paint_order() {
usvg::PaintOrder::FillAndStroke => {
if let Some(paint) = fpaint { canvas. fill_path(&fpath, &paint); }
if let Some(paint) = lpaint { canvas.stroke_path(&fpath, &paint); }
}
usvg::PaintOrder::StrokeAndFill => {
if let Some(paint) = lpaint { canvas.stroke_path(&fpath, &paint); }
if let Some(paint) = fpaint { canvas. fill_path(&fpath, &paint); }
}
}
if canvas.contains_point(&fpath, mouse.0, mouse.1, FillRule::NonZero) {
canvas.stroke_path(&fpath, &Paint::color(Color::rgb(32, 240, 32))
.with_line_width(1. / canvas.transform()[0]));
}
}
usvg::Node::Image(img) => if img.is_visible() {
match img.kind() { usvg::ImageKind::JPEG(_) |
usvg::ImageKind::PNG(_) | usvg::ImageKind::GIF(_) => todo!(),
usvg::ImageKind::SVG(svg) =>
render_nodes(canvas, mouse, svg.root(), trfm),
}
}
usvg::Node::Text(text) => { let group = text.flattened();
render_nodes(canvas, mouse, group, &trfm.pre_concat(group.transform()));
}
} }
}
fn _some_test_case<T: Renderer>(canvas: &mut Canvas<T>) {
let (w, h) = (canvas.width(), canvas.height());
let (w, h) = (w as f32, h as f32);
let (lx, ty) = (w / 4., h / 4.);
let mut path = Path::new(); path.rect(lx, ty, w / 2., h / 2.);
canvas.stroke_path(&path, &Paint::color(Color::rgbaf(0., 0., 1., 1.)).with_line_width(1.));
canvas.fill_path(&path, &Paint::color(Color::rgbaf(1., 0.5, 0.5, 1.)));
canvas.global_composite_operation(femtovg::CompositeOperation::DestinationIn);
let mut path = Path::new();
let (rx, by) = (w - lx, h - ty - 10.);
path.move_to(w / 2., ty); path.line_to(rx, by); path.line_to(lx, by); path.close();
canvas.fill_path(&path, &Paint::color(Color::rgbaf(0., 1., 0., 1.)));
canvas.global_composite_operation(femtovg::CompositeOperation::SourceOver);
}