Integrate Vello for vector rendering (#1802)

* Start integrating vello into render pipeline

Cache vello render creation

Implement viewport navigation

Close vello path

Add transform parameter to vello render pass

* Fix render node types

* Fix a bunch of bugs in the path translation

* Avoid panic on empty document

* Fix rendering of holes

* Implement image rendering

* Implement graph recompilation afer editor api change

* Implement preferences toggle for using vello as the renderer

* Make surface creation optional

* Feature gate vello usages

* Implement skeleton for radial gradient

* Rename vello preference

* Fix some gradients

* Only update monitor nodes on graph recompile

* Fix warnings + remove dead code

* Update everything except for thumbnails after a node graph evaluation

* Fix missing click targets for Image frames

* Improve perfamance by removing unecessary widget updates

* Fix node graph paning

* Fix thumbnail loading

* Implement proper hash for vector modification

* Fix test and warnings

* Code review

* Fix dep

* Remove warning

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2024-07-22 01:56:29 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent 8e774efe9d
commit ab71d26d84
41 changed files with 805 additions and 758 deletions
+157 -111
View File
@@ -4,6 +4,7 @@ use crate::raster::bbox::Bbox;
use crate::raster::{BlendMode, Image, ImageFrame};
use crate::transform::Transform;
use crate::uuid::generate_uuid;
use crate::vector::style::{Fill, Stroke, ViewMode};
use crate::vector::PointId;
use crate::SurfaceFrame;
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
@@ -13,6 +14,8 @@ use bezier_rs::Subpath;
use base64::Engine;
use glam::{DAffine2, DVec2};
#[cfg(feature = "vello")]
use vello::*;
/// Represents a clickable target for the layer
#[derive(Clone, Debug)]
@@ -166,7 +169,7 @@ pub enum ImageRenderMode {
/// Static state used whilst rendering
#[derive(Default)]
pub struct RenderParams {
pub view_mode: crate::vector::style::ViewMode,
pub view_mode: ViewMode,
pub image_render_mode: ImageRenderMode,
pub culling_bounds: Option<[DVec2; 2]>,
pub thumbnail: bool,
@@ -177,7 +180,7 @@ pub struct RenderParams {
}
impl RenderParams {
pub fn new(view_mode: crate::vector::style::ViewMode, image_render_mode: ImageRenderMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self {
pub fn new(view_mode: ViewMode, image_render_mode: ImageRenderMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self {
Self {
view_mode,
image_render_mode,
@@ -203,7 +206,7 @@ pub fn format_transform_matrix(transform: DAffine2) -> String {
result
}
fn to_transform(transform: DAffine2) -> usvg::Transform {
pub fn to_transform(transform: DAffine2) -> usvg::Transform {
let cols = transform.to_cols_array();
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
}
@@ -212,33 +215,14 @@ pub trait GraphicElementRendered {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams);
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>;
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>);
fn to_usvg_node(&self) -> usvg::Node {
let mut render = SvgRender::new();
let render_params = RenderParams::new(crate::vector::style::ViewMode::Normal, ImageRenderMode::Base64, None, false, false, false);
self.render_svg(&mut render, &render_params);
render.format_svg(DVec2::ZERO, DVec2::ONE);
let svg = render.svg.to_svg_string();
let opt = usvg::Options::default();
let tree = usvg::Tree::from_str(&svg, &opt).expect("Failed to parse SVG");
usvg::Node::Group(Box::new(tree.root.clone()))
}
fn to_usvg_tree(&self, resolution: glam::UVec2, viewbox: [DVec2; 2]) -> usvg::Tree {
let root = match self.to_usvg_node() {
usvg::Node::Group(root_node) => *root_node,
_ => usvg::Group::default(),
};
usvg::Tree {
size: usvg::Size::from_wh(resolution.x as f32, resolution.y as f32).unwrap(),
view_box: usvg::ViewBox {
rect: usvg::NonZeroRect::from_ltrb(viewbox[0].x as f32, viewbox[0].y as f32, viewbox[1].x as f32, viewbox[1].y as f32).unwrap(),
aspect: usvg::AspectRatio::default(),
},
root,
}
#[cfg(feature = "vello")]
fn to_vello_scene(&self, transform: DAffine2) -> Scene {
let mut scene = vello::Scene::new();
self.render_to_vello(&mut scene, transform);
scene
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2) {}
fn contains_artboard(&self) -> bool {
false
@@ -283,12 +267,22 @@ impl GraphicElementRendered for GraphicGroup {
}
}
fn to_usvg_node(&self) -> usvg::Node {
let mut root_node = usvg::Group::default();
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2) {
let kurbo_transform = kurbo::Affine::new((transform * self.transform).to_cols_array());
// TODO: We make the bounding box bigger to accommodate for the stroke width. This should be done in a better way.
let Some(bounds) = self.bounding_box(DAffine2::from_scale(DVec2::splat(1.05))) else { return };
let blending = vello::peniko::BlendMode::new(self.alpha_blending.blend_mode.into(), vello::peniko::Compose::SrcOver);
scene.push_layer(
blending,
self.alpha_blending.opacity,
kurbo_transform,
&vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
);
for element in self.iter() {
root_node.children.push(element.to_usvg_node());
element.render_to_vello(scene, transform * self.transform);
}
usvg::Node::Group(Box::new(root_node))
scene.pop_layer();
}
fn contains_artboard(&self) -> bool {
@@ -335,8 +329,8 @@ impl GraphicElementRendered for VectorData {
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight);
let filled = self.style.fill() != &crate::vector::style::Fill::None;
let stroke_width = self.style.stroke().as_ref().map_or(0., Stroke::weight);
let filled = self.style.fill() != &Fill::None;
let fill = |mut subpath: bezier_rs::Subpath<_>| {
if filled {
subpath.set_closed(true);
@@ -346,38 +340,81 @@ impl GraphicElementRendered for VectorData {
click_targets.extend(self.stroke_bezier_paths().map(fill).map(|subpath| ClickTarget { stroke_width, subpath }));
}
fn to_usvg_node(&self) -> usvg::Node {
use bezier_rs::BezierHandles;
use usvg::tiny_skia_path::PathBuilder;
let mut builder = PathBuilder::new();
let vector_data = self;
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2) {
use crate::vector::style::GradientType;
use vello::peniko;
let transform = to_transform(vector_data.transform);
for subpath in vector_data.stroke_bezier_paths() {
let start = vector_data.transform.transform_point2(subpath[0].anchor);
builder.move_to(start.x as f32, start.y as f32);
for bezier in subpath.iter() {
bezier.apply_transformation(|pos| vector_data.transform.transform_point2(pos));
let end = bezier.end;
match bezier.handles {
BezierHandles::Linear => builder.line_to(end.x as f32, end.y as f32),
BezierHandles::Quadratic { handle } => builder.quad_to(handle.x as f32, handle.y as f32, end.x as f32, end.y as f32),
BezierHandles::Cubic { handle_start, handle_end } => {
builder.cubic_to(handle_start.x as f32, handle_start.y as f32, handle_end.x as f32, handle_end.y as f32, end.x as f32, end.y as f32)
}
let kurbo_transform = kurbo::Affine::new(transform.to_cols_array());
let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y);
let mut path = kurbo::BezPath::new();
for (_, subpath) in self.region_bezier_paths() {
subpath.to_vello_path(self.transform, &mut path);
}
match self.style.fill() {
Fill::Solid(color) => {
let fill = peniko::Brush::Solid(peniko::Color::rgba(color.r() as f64, color.g() as f64, color.b() as f64, color.a() as f64));
scene.fill(peniko::Fill::NonZero, kurbo_transform, &fill, None, &path);
}
Fill::Gradient(gradient) => {
let mut stops = peniko::ColorStops::new();
for &(offset, color) in &gradient.stops.0 {
stops.push(peniko::ColorStop {
offset: offset as f32,
color: peniko::Color::rgba(color.r() as f64, color.g() as f64, color.b() as f64, color.a() as f64),
});
}
}
if subpath.closed {
builder.close()
// Compute bounding box of the shape to determine the gradient start and end points
let bounds = self.bounding_box().unwrap_or_default();
let lerp_bounds = |p: DVec2| bounds[0] + (bounds[1] - bounds[0]) * p;
let start = lerp_bounds(gradient.start);
let end = lerp_bounds(gradient.end);
let transform = self.transform * gradient.transform;
let start = transform.transform_point2(start);
let end = transform.transform_point2(end);
let fill = peniko::Brush::Gradient(peniko::Gradient {
kind: match gradient.gradient_type {
GradientType::Linear => peniko::GradientKind::Linear {
start: to_point(start),
end: to_point(end),
},
GradientType::Radial => {
let radius = start.distance(end);
peniko::GradientKind::Radial {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(end),
end_radius: radius as f32,
}
}
},
stops,
..Default::default()
});
scene.fill(peniko::Fill::NonZero, kurbo_transform, &fill, None, &path);
}
Fill::None => (),
};
let mut path = kurbo::BezPath::new();
for (_, subpath) in self.region_bezier_paths() {
subpath.to_vello_path(self.transform, &mut path);
}
if let Some(stroke) = self.style.stroke() {
let color = match stroke.color {
Some(color) => peniko::Color::rgba(color.r() as f64, color.g() as f64, color.b() as f64, color.a() as f64),
None => peniko::Color::TRANSPARENT,
};
let stroke = kurbo::Stroke {
width: stroke.weight,
miter_limit: stroke.line_join_miter_limit,
..Default::default()
};
scene.stroke(&stroke, kurbo_transform, color, None, &path);
}
let path = builder.finish().unwrap();
let mut path = usvg::Path::new(path.into());
path.abs_transform = transform;
// TODO: use proper style
path.fill = None;
path.stroke = Some(usvg::Stroke::default());
usvg::Node::Path(Box::new(path))
}
}
@@ -458,6 +495,28 @@ impl GraphicElementRendered for Artboard {
}
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2) {
use vello::peniko;
// Render background
let color = peniko::Color::rgba(self.background.r() as f64, self.background.g() as f64, self.background.b() as f64, self.background.a() as f64);
let rect = kurbo::Rect::new(self.location.x as f64, self.location.y as f64, self.dimensions.x as f64, self.dimensions.y as f64);
let blend_mode = peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcOver);
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), color, None, &rect);
scene.pop_layer();
if self.clip {
scene.push_layer(blend_mode, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
}
self.graphic_group.render_to_vello(scene, transform * self.transform());
if self.clip {
scene.pop_layer();
}
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
subpath.apply_transform(self.graphic_group.transform.inverse());
@@ -486,6 +545,13 @@ impl GraphicElementRendered for crate::ArtboardGroup {
}
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2) {
for artboard in &self.artboards {
artboard.render_to_vello(scene, transform)
}
}
fn contains_artboard(&self) -> bool {
!self.artboards.is_empty()
}
@@ -511,6 +577,11 @@ impl GraphicElementRendered for SurfaceFrame {
render.svg.push(canvas.into())
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2) {
todo!()
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let bbox = Bbox::from_transform(transform);
let aabb = bbox.to_axis_aligned_bbox();
@@ -567,24 +638,24 @@ impl GraphicElementRendered for ImageFrame<Color> {
click_targets.push(ClickTarget { subpath, stroke_width: 0. });
}
fn to_usvg_node(&self) -> usvg::Node {
let image_frame = self;
if image_frame.image.width * image_frame.image.height == 0 {
return usvg::Node::Group(Box::default());
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2) {
use vello::peniko;
let image = &self.image;
if image.data.is_empty() {
return;
}
let png = image_frame.image.to_png();
usvg::Node::Image(Box::new(usvg::Image {
id: String::new(),
abs_transform: to_transform(image_frame.transform),
visibility: usvg::Visibility::Visible,
view_box: usvg::ViewBox {
rect: usvg::NonZeroRect::from_xywh(0., 0., 1., 1.).unwrap(),
aspect: usvg::AspectRatio::default(),
},
rendering_mode: usvg::ImageRendering::OptimizeSpeed,
kind: usvg::ImageKind::PNG(png.into()),
bounding_box: None,
}))
let image = vello::peniko::Image {
data: image.to_flat_u8().0.into(),
width: image.width,
height: image.height,
format: peniko::Format::Rgba8,
extend: peniko::Extend::Repeat,
};
let transform = transform * self.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
scene.draw_image(&image, vello::kurbo::Affine::new(transform.to_cols_array()));
}
}
@@ -616,12 +687,13 @@ impl GraphicElementRendered for GraphicElement {
}
}
fn to_usvg_node(&self) -> usvg::Node {
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2) {
match self {
GraphicElement::VectorData(vector_data) => vector_data.to_usvg_node(),
GraphicElement::ImageFrame(image_frame) => image_frame.to_usvg_node(),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.to_usvg_node(),
GraphicElement::Surface(surface) => surface.to_usvg_node(),
GraphicElement::VectorData(vector_data) => vector_data.render_to_vello(scene, transform),
GraphicElement::ImageFrame(image_frame) => image_frame.render_to_vello(scene, transform),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_to_vello(scene, transform),
GraphicElement::Surface(surface) => surface.render_to_vello(scene, transform),
}
}
@@ -659,32 +731,6 @@ impl<T: Primitive> GraphicElementRendered for T {
}
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
fn to_usvg_node(&self) -> usvg::Node {
let text = self;
usvg::Node::Text(Box::new(usvg::Text {
id: String::new(),
abs_transform: usvg::Transform::identity(),
rendering_mode: usvg::TextRendering::OptimizeSpeed,
writing_mode: usvg::WritingMode::LeftToRight,
chunks: vec![usvg::TextChunk {
text: text.to_string(),
x: None,
y: None,
anchor: usvg::TextAnchor::Start,
spans: vec![],
text_flow: usvg::TextFlow::Linear,
}],
dx: Vec::new(),
dy: Vec::new(),
rotate: Vec::new(),
bounding_box: None,
abs_bounding_box: None,
stroke_bounding_box: None,
abs_stroke_bounding_box: None,
flattened: None,
}))
}
}
impl GraphicElementRendered for Option<Color> {