mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Implement clipping masks, stroke align, and stroke paint order (#2644)
* refactor: opacity + blend_mode -> blend_style * Add code for clipping * Add alt-click masking * Clip to all colors. Fill option * Fix undo not working. Fix strokes not being white * Allow clipped to be grouped or raster * Switch to alpha mode in mask-type * add plumbing to know if clipped in frontend and add fill slider * Attempt at document upgrade code * Fix fill slider * Add clipped styling and Alt-click layer border * Use mask attr judiciously by using clip when possible * Fix breaking documents and upgrade code * Fix fixes * No-op toggle if last child of parent and don't show clip UI if last element * Fix mouse styles by plumbing clippable to frontend * Fix Clip detection by disallowed groups as clipPath according to SVG spec doesn't allow <g> * Add opacity to clippers can_use_clip check * Fix issue with clipping not working nicely with strokes by using masks * Add vello code * cleanup * Add stroke alignment hacks to SVG renderer * svg: Fix mask bounds in vector data * vello: Implement mask hacks to support stroke alignment * Move around alignment and doc upgrade code * rename Line X -> X * An attempt at fixing names not updating * svg: add stroke order with svg * vello: add stroke order with by calling one before the other explicitly * fix merge * fix svg renderer messing up transform det * Code review; reorder and rename parameters (TODO: fix tools) * Fixes to previous * Formatting * fix bug 3 * some moving around (not fixed) * fix issue 1 * fix vello * Final code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
8a3f133140
commit
e238753a35
28 changed files with 1025 additions and 311 deletions
|
@ -14,9 +14,12 @@ pub mod renderer;
|
|||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[serde(default)]
|
||||
pub struct AlphaBlending {
|
||||
pub opacity: f32,
|
||||
pub blend_mode: BlendMode,
|
||||
pub opacity: f32,
|
||||
pub fill: f32,
|
||||
pub clip: bool,
|
||||
}
|
||||
impl Default for AlphaBlending {
|
||||
fn default() -> Self {
|
||||
|
@ -26,13 +29,22 @@ impl Default for AlphaBlending {
|
|||
impl core::hash::Hash for AlphaBlending {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.opacity.to_bits().hash(state);
|
||||
self.fill.to_bits().hash(state);
|
||||
self.blend_mode.hash(state);
|
||||
self.clip.hash(state);
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for AlphaBlending {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let round = |x: f32| (x * 1e3).round() / 1e3;
|
||||
write!(f, "Opacity: {}% — Blend Mode: {}", round(self.opacity * 100.), self.blend_mode)
|
||||
write!(
|
||||
f,
|
||||
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
|
||||
self.blend_mode,
|
||||
round(self.opacity * 100.),
|
||||
round(self.fill * 100.),
|
||||
if self.clip { "Yes" } else { "No" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,7 +52,9 @@ impl AlphaBlending {
|
|||
pub const fn new() -> Self {
|
||||
Self {
|
||||
opacity: 1.,
|
||||
fill: 1.,
|
||||
blend_mode: BlendMode::Normal,
|
||||
clip: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +63,9 @@ impl AlphaBlending {
|
|||
|
||||
AlphaBlending {
|
||||
opacity: lerp(self.opacity, other.opacity, t),
|
||||
fill: lerp(self.fill, other.fill, t),
|
||||
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
|
||||
clip: if t < 0.5 { self.clip } else { other.clip },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,6 +221,26 @@ impl GraphicElement {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn had_clip_enabled(&self) -> bool {
|
||||
match self {
|
||||
GraphicElement::VectorData(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
|
||||
GraphicElement::GraphicGroup(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
|
||||
GraphicElement::RasterDataCPU(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
|
||||
GraphicElement::RasterDataGPU(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_reduce_to_clip_path(&self) -> bool {
|
||||
match self {
|
||||
GraphicElement::VectorData(vector_data_table) => vector_data_table.instance_ref_iter().all(|instance_data| {
|
||||
let style = &instance_data.instance.style;
|
||||
let alpha_blending = &instance_data.alpha_blending;
|
||||
(alpha_blending.opacity > 1. - f32::EPSILON) && style.fill().is_opaque() && style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // TODO: Rename to Raster
|
||||
|
@ -448,8 +484,10 @@ async fn flatten_vector(_: impl Ctx, group: GraphicGroupTable) -> VectorDataTabl
|
|||
instance: current_element.instance.clone(),
|
||||
transform: *current_instance.transform * *current_element.transform,
|
||||
alpha_blending: AlphaBlending {
|
||||
opacity: current_instance.alpha_blending.opacity * current_element.alpha_blending.opacity,
|
||||
blend_mode: current_element.alpha_blending.blend_mode,
|
||||
opacity: current_instance.alpha_blending.opacity * current_element.alpha_blending.opacity,
|
||||
fill: current_element.alpha_blending.fill,
|
||||
clip: current_element.alpha_blending.clip,
|
||||
},
|
||||
source_node_id: reference,
|
||||
});
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
mod quad;
|
||||
mod rect;
|
||||
|
||||
use crate::instances::Instance;
|
||||
use crate::raster::{BlendMode, Image};
|
||||
use crate::raster_types::{CPU, GPU, RasterDataTable};
|
||||
use crate::transform::{Footprint, Transform};
|
||||
use crate::uuid::{NodeId, generate_uuid};
|
||||
use crate::vector::style::{Fill, Stroke, ViewMode};
|
||||
use crate::vector::style::{Fill, Stroke, StrokeAlign, ViewMode};
|
||||
use crate::vector::{PointId, VectorDataTable};
|
||||
use crate::{Artboard, ArtboardGroupTable, Color, GraphicElement, GraphicGroupTable};
|
||||
use base64::Engine;
|
||||
|
@ -50,6 +51,29 @@ pub struct ClickTarget {
|
|||
bounding_box: Option<[DVec2; 2]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
enum MaskType {
|
||||
Clip,
|
||||
Mask,
|
||||
}
|
||||
|
||||
impl MaskType {
|
||||
fn to_attribute(self) -> String {
|
||||
match self {
|
||||
Self::Mask => "mask".to_string(),
|
||||
Self::Clip => "clip-path".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_defs(self, svg_defs: &mut String, uuid: u64, svg_string: String) {
|
||||
let id = format!("mask-{}", uuid);
|
||||
match self {
|
||||
Self::Clip => write!(svg_defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg_string).unwrap(),
|
||||
Self::Mask => write!(svg_defs, r##"<mask id="{id}" mask-type="alpha">{}</mask>"##, svg_string).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClickTarget {
|
||||
pub fn new_with_subpath(subpath: bezier_rs::Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
let bounding_box = subpath.loose_bounding_box();
|
||||
|
@ -289,17 +313,20 @@ pub struct RenderParams {
|
|||
pub hide_artboards: bool,
|
||||
/// Are we exporting? Causes the text above an artboard to be hidden.
|
||||
pub for_export: bool,
|
||||
/// Are we generating a mask in this render pass? Used to see if fill should be multiplied with alpha.
|
||||
pub for_mask: bool,
|
||||
/// Are we generating a mask for alignment? Used to prevent unnecesary transforms in masks
|
||||
pub alignment_parent_transform: Option<DAffine2>,
|
||||
}
|
||||
|
||||
impl RenderParams {
|
||||
pub fn new(view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self {
|
||||
Self {
|
||||
view_mode,
|
||||
culling_bounds,
|
||||
thumbnail,
|
||||
hide_artboards,
|
||||
for_export,
|
||||
}
|
||||
pub fn for_clipper(&self) -> Self {
|
||||
Self { for_mask: true, ..*self }
|
||||
}
|
||||
|
||||
pub fn for_alignment(&self, transform: DAffine2) -> Self {
|
||||
let alignment_parent_transform = Some(transform);
|
||||
Self { alignment_parent_transform, ..*self }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,7 +389,10 @@ pub trait GraphicElementRendered {
|
|||
|
||||
impl GraphicElementRendered for GraphicGroupTable {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
for instance in self.instance_ref_iter() {
|
||||
let mut iter = self.instance_ref_iter().peekable();
|
||||
let mut mask_state = None;
|
||||
|
||||
while let Some(instance) = iter.next() {
|
||||
render.parent_tag(
|
||||
"g",
|
||||
|attributes| {
|
||||
|
@ -371,13 +401,37 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
attributes.push("transform", matrix);
|
||||
}
|
||||
|
||||
if instance.alpha_blending.opacity < 1. {
|
||||
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
|
||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
||||
let opacity = instance.alpha_blending.opacity * factor;
|
||||
if opacity < 1. {
|
||||
attributes.push("opacity", opacity.to_string());
|
||||
}
|
||||
|
||||
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
attributes.push("style", instance.alpha_blending.blend_mode.render());
|
||||
}
|
||||
|
||||
let next_clips = iter.peek().is_some_and(|next_instance| next_instance.instance.had_clip_enabled());
|
||||
|
||||
if next_clips && mask_state.is_none() {
|
||||
let uuid = generate_uuid();
|
||||
let mask_type = if instance.instance.can_reduce_to_clip_path() { MaskType::Clip } else { MaskType::Mask };
|
||||
mask_state = Some((uuid, mask_type));
|
||||
let mut svg = SvgRender::new();
|
||||
instance.instance.render_svg(&mut svg, &render_params.for_clipper());
|
||||
|
||||
write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap();
|
||||
mask_type.write_to_defs(&mut attributes.0.svg_defs, uuid, svg.svg.to_svg_string());
|
||||
} else if let Some((uuid, mask_type)) = mask_state {
|
||||
if !next_clips {
|
||||
mask_state = None;
|
||||
}
|
||||
|
||||
let id = format!("mask-{}", uuid);
|
||||
let selector = format!("url(#{id})");
|
||||
|
||||
attributes.push(mask_type.to_attribute(), selector);
|
||||
}
|
||||
},
|
||||
|render| {
|
||||
instance.instance.render_svg(render, render_params);
|
||||
|
@ -388,25 +442,31 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
|
||||
#[cfg(feature = "vello")]
|
||||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
|
||||
for instance in self.instance_ref_iter() {
|
||||
let mut iter = self.instance_ref_iter().peekable();
|
||||
let mut mask_instance_state = None;
|
||||
|
||||
while let Some(instance) = iter.next() {
|
||||
let transform = transform * *instance.transform;
|
||||
let alpha_blending = *instance.alpha_blending;
|
||||
|
||||
let mut layer = false;
|
||||
if let Some(bounds) = self
|
||||
|
||||
let bounds = self
|
||||
.instance_ref_iter()
|
||||
.filter_map(|element| element.instance.bounding_box(transform, true))
|
||||
.reduce(Quad::combine_bounds)
|
||||
{
|
||||
.reduce(Quad::combine_bounds);
|
||||
if let Some(bounds) = bounds {
|
||||
let blend_mode = match render_params.view_mode {
|
||||
ViewMode::Outline => peniko::Mix::Normal,
|
||||
_ => alpha_blending.blend_mode.into(),
|
||||
};
|
||||
|
||||
if alpha_blending.opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
|
||||
let factor = if render_params.for_mask { 1. } else { alpha_blending.fill };
|
||||
let opacity = alpha_blending.opacity * factor;
|
||||
if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
|
||||
scene.push_layer(
|
||||
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
|
||||
alpha_blending.opacity,
|
||||
opacity,
|
||||
kurbo::Affine::IDENTITY,
|
||||
&vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
|
||||
);
|
||||
|
@ -414,7 +474,33 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
}
|
||||
}
|
||||
|
||||
instance.instance.render_to_vello(scene, transform, context, render_params);
|
||||
let next_clips = iter.peek().is_some_and(|next_instance| next_instance.instance.had_clip_enabled());
|
||||
if next_clips && mask_instance_state.is_none() {
|
||||
mask_instance_state = Some((instance.instance, transform));
|
||||
|
||||
instance.instance.render_to_vello(scene, transform, context, render_params);
|
||||
} else if let Some((instance_mask, transform_mask)) = mask_instance_state {
|
||||
if !next_clips {
|
||||
mask_instance_state = None;
|
||||
}
|
||||
|
||||
if let Some(bounds) = bounds {
|
||||
let rect = vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
|
||||
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
|
||||
instance_mask.render_to_vello(scene, transform_mask, context, &render_params.for_clipper());
|
||||
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcIn), 1., kurbo::Affine::IDENTITY, &rect);
|
||||
}
|
||||
|
||||
instance.instance.render_to_vello(scene, transform, context, render_params);
|
||||
|
||||
if bounds.is_some() {
|
||||
scene.pop_layer();
|
||||
scene.pop_layer();
|
||||
}
|
||||
} else {
|
||||
instance.instance.render_to_vello(scene, transform, context, render_params);
|
||||
}
|
||||
|
||||
if layer {
|
||||
scene.pop_layer();
|
||||
|
@ -488,21 +574,54 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
impl GraphicElementRendered for VectorDataTable {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
for instance in self.instance_ref_iter() {
|
||||
let multiplied_transform = render.transform * *instance.transform;
|
||||
let multiplied_transform = *instance.transform;
|
||||
let vector_data = &instance.instance;
|
||||
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
|
||||
let has_real_stroke = instance.instance.style.stroke().filter(|stroke| stroke.weight() > 0.);
|
||||
let has_real_stroke = vector_data.style.stroke().filter(|stroke| stroke.weight() > 0.);
|
||||
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
|
||||
let applied_stroke_transform = set_stroke_transform.unwrap_or(*instance.transform);
|
||||
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
|
||||
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
|
||||
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
let layer_bounds = instance.instance.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = instance.instance.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
|
||||
let layer_bounds = vector_data.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = vector_data.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
|
||||
|
||||
let mut path = String::new();
|
||||
|
||||
for subpath in instance.instance.stroke_bezier_paths() {
|
||||
let _ = subpath.subpath_to_svg(&mut path, applied_stroke_transform);
|
||||
}
|
||||
|
||||
let connected = vector_data.stroke_bezier_paths().all(|path| path.closed());
|
||||
let can_draw_aligned_stroke = vector_data.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && connected;
|
||||
let mut push_id = None;
|
||||
|
||||
if can_draw_aligned_stroke {
|
||||
let mask_type = if vector_data.style.stroke().unwrap().align == StrokeAlign::Inside {
|
||||
MaskType::Clip
|
||||
} else {
|
||||
MaskType::Mask
|
||||
};
|
||||
|
||||
let can_use_order = !instance.instance.style.fill().is_none() && mask_type == MaskType::Mask;
|
||||
if !can_use_order {
|
||||
let id = format!("alignment-{}", generate_uuid());
|
||||
let mut vector_row = VectorDataTable::default();
|
||||
let mut fill_instance = instance.instance.clone();
|
||||
|
||||
fill_instance.style.clear_stroke();
|
||||
fill_instance.style.set_fill(Fill::solid(Color::BLACK));
|
||||
|
||||
vector_row.push(Instance {
|
||||
instance: fill_instance,
|
||||
alpha_blending: *instance.alpha_blending,
|
||||
transform: *instance.transform,
|
||||
source_node_id: None,
|
||||
});
|
||||
push_id = Some((id, mask_type, vector_row));
|
||||
}
|
||||
}
|
||||
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path);
|
||||
let matrix = format_transform_matrix(element_transform);
|
||||
|
@ -511,15 +630,43 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
}
|
||||
|
||||
let defs = &mut attributes.0.svg_defs;
|
||||
if let Some((ref id, mask_type, ref vector_row)) = push_id {
|
||||
let mut svg = SvgRender::new();
|
||||
vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
|
||||
|
||||
let fill_and_stroke = instance
|
||||
.instance
|
||||
.style
|
||||
.render(render_params.view_mode, defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds);
|
||||
let weight = instance.instance.style.stroke().unwrap().weight * instance.transform.matrix2.determinant();
|
||||
let quad = Quad::from_box(transformed_bounds).inflate(weight);
|
||||
let (x, y) = quad.top_left().into();
|
||||
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
|
||||
write!(defs, r##"{}"##, svg.svg_defs).unwrap();
|
||||
let rect = format!(r##"<rect x="{}" y="{}" width="{width}" height="{height}" fill="white" />"##, x, y);
|
||||
match mask_type {
|
||||
MaskType::Clip => write!(defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg.svg.to_svg_string()).unwrap(),
|
||||
MaskType::Mask => write!(defs, r##"<mask id="{id}">{}{}</mask>"##, rect, svg.svg.to_svg_string()).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
let fill_and_stroke = instance.instance.style.render(
|
||||
defs,
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
layer_bounds,
|
||||
transformed_bounds,
|
||||
can_draw_aligned_stroke,
|
||||
can_draw_aligned_stroke && push_id.is_none(),
|
||||
render_params,
|
||||
);
|
||||
|
||||
if let Some((id, mask_type, _)) = push_id {
|
||||
let selector = format!("url(#{id})");
|
||||
attributes.push(mask_type.to_attribute(), selector);
|
||||
}
|
||||
attributes.push_val(fill_and_stroke);
|
||||
|
||||
if instance.alpha_blending.opacity < 1. {
|
||||
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
|
||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
||||
let opacity = instance.alpha_blending.opacity * factor;
|
||||
if opacity < 1. {
|
||||
attributes.push("opacity", opacity.to_string());
|
||||
}
|
||||
|
||||
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
|
@ -530,9 +677,9 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
}
|
||||
|
||||
#[cfg(feature = "vello")]
|
||||
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) {
|
||||
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
|
||||
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
|
||||
use crate::vector::style::{GradientType, LineCap, LineJoin};
|
||||
use crate::vector::style::{GradientType, StrokeCap, StrokeJoin};
|
||||
use vello::kurbo::{Cap, Join};
|
||||
use vello::peniko;
|
||||
|
||||
|
@ -541,6 +688,7 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
let has_real_stroke = instance.instance.style.stroke().filter(|stroke| stroke.weight() > 0.);
|
||||
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
|
||||
let applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
|
||||
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
|
||||
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
|
||||
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
let layer_bounds = instance.instance.bounding_box().unwrap_or_default();
|
||||
|
@ -557,16 +705,51 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
_ => instance.alpha_blending.blend_mode.into(),
|
||||
};
|
||||
let mut layer = false;
|
||||
if instance.alpha_blending.opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
||||
let opacity = instance.alpha_blending.opacity * factor;
|
||||
if opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
layer = true;
|
||||
scene.push_layer(
|
||||
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
|
||||
instance.alpha_blending.opacity,
|
||||
opacity,
|
||||
kurbo::Affine::new(multiplied_transform.to_cols_array()),
|
||||
&kurbo::Rect::new(layer_bounds[0].x, layer_bounds[0].y, layer_bounds[1].x, layer_bounds[1].y),
|
||||
);
|
||||
}
|
||||
|
||||
let can_draw_aligned_stroke = instance.instance.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
|
||||
&& instance.instance.stroke_bezier_paths().all(|path| path.closed());
|
||||
|
||||
let reorder_for_outside = instance
|
||||
.instance
|
||||
.style
|
||||
.stroke()
|
||||
.is_some_and(|stroke| stroke.align == StrokeAlign::Outside && !instance.instance.style.fill().is_none());
|
||||
if can_draw_aligned_stroke && !reorder_for_outside {
|
||||
let mut vector_data = VectorDataTable::default();
|
||||
|
||||
let mut fill_instance = instance.instance.clone();
|
||||
fill_instance.style.clear_stroke();
|
||||
fill_instance.style.set_fill(Fill::solid(Color::BLACK));
|
||||
|
||||
vector_data.push(Instance {
|
||||
instance: fill_instance,
|
||||
alpha_blending: *instance.alpha_blending,
|
||||
transform: *instance.transform,
|
||||
source_node_id: None,
|
||||
});
|
||||
|
||||
let weight = instance.instance.style.stroke().unwrap().weight;
|
||||
let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant());
|
||||
let rect = vello::kurbo::Rect::new(quad.top_left().x, quad.top_left().y, quad.bottom_right().x, quad.bottom_right().y);
|
||||
|
||||
let inside = instance.instance.style.stroke().unwrap().align == StrokeAlign::Inside;
|
||||
let compose = if inside { peniko::Compose::SrcIn } else { peniko::Compose::SrcOut };
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
|
||||
vector_data.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform));
|
||||
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect);
|
||||
}
|
||||
|
||||
// Render the path
|
||||
match render_params.view_mode {
|
||||
ViewMode::Outline => {
|
||||
|
@ -589,90 +772,111 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path);
|
||||
}
|
||||
_ => {
|
||||
match instance.instance.style.fill() {
|
||||
Fill::Solid(color) => {
|
||||
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
});
|
||||
}
|
||||
// Compute bounding box of the shape to determine the gradient start and end points
|
||||
let bounds = instance.instance.nonzero_bounding_box();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
enum Op {
|
||||
Fill,
|
||||
Stroke,
|
||||
}
|
||||
|
||||
let inverse_parent_transform = (parent_transform.matrix2.determinant() != 0.).then(|| parent_transform.inverse()).unwrap_or_default();
|
||||
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
|
||||
|
||||
let start = mod_points.transform_point2(gradient.start);
|
||||
let end = mod_points.transform_point2(gradient.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(start),
|
||||
end_radius: radius as f32,
|
||||
}
|
||||
}
|
||||
},
|
||||
stops,
|
||||
..Default::default()
|
||||
});
|
||||
// Vello does `element_transform * brush_transform` internally. We don't want element_transform to have any impact so we need to left multiply by the inverse.
|
||||
// This makes the final internal brush transform equal to `parent_transform`, allowing you to stretch a gradient by transforming the parent folder.
|
||||
let inverse_element_transform = (element_transform.matrix2.determinant() != 0.).then(|| element_transform.inverse()).unwrap_or_default();
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
|
||||
}
|
||||
Fill::None => {}
|
||||
let order = match instance.instance.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) || reorder_for_outside {
|
||||
true => [Op::Stroke, Op::Fill],
|
||||
false => [Op::Fill, Op::Stroke], // Default
|
||||
};
|
||||
for operation in order {
|
||||
match operation {
|
||||
Op::Fill => {
|
||||
match instance.instance.style.fill() {
|
||||
Fill::Solid(color) => {
|
||||
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
});
|
||||
}
|
||||
// Compute bounding box of the shape to determine the gradient start and end points
|
||||
let bounds = instance.instance.nonzero_bounding_box();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
|
||||
if let Some(stroke) = instance.instance.style.stroke() {
|
||||
let color = match stroke.color {
|
||||
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
|
||||
None => peniko::Color::TRANSPARENT,
|
||||
};
|
||||
let cap = match stroke.line_cap {
|
||||
LineCap::Butt => Cap::Butt,
|
||||
LineCap::Round => Cap::Round,
|
||||
LineCap::Square => Cap::Square,
|
||||
};
|
||||
let join = match stroke.line_join {
|
||||
LineJoin::Miter => Join::Miter,
|
||||
LineJoin::Bevel => Join::Bevel,
|
||||
LineJoin::Round => Join::Round,
|
||||
};
|
||||
let stroke = kurbo::Stroke {
|
||||
width: stroke.weight,
|
||||
miter_limit: stroke.line_join_miter_limit,
|
||||
join,
|
||||
start_cap: cap,
|
||||
end_cap: cap,
|
||||
dash_pattern: stroke.dash_lengths.into(),
|
||||
dash_offset: stroke.dash_offset,
|
||||
};
|
||||
let inverse_parent_transform = (parent_transform.matrix2.determinant() != 0.).then(|| parent_transform.inverse()).unwrap_or_default();
|
||||
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
|
||||
|
||||
// Draw the stroke if it's visible
|
||||
if stroke.width > 0. {
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
|
||||
let start = mod_points.transform_point2(gradient.start);
|
||||
let end = mod_points.transform_point2(gradient.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(start),
|
||||
end_radius: radius as f32,
|
||||
}
|
||||
}
|
||||
},
|
||||
stops,
|
||||
..Default::default()
|
||||
});
|
||||
// Vello does `element_transform * brush_transform` internally. We don't want element_transform to have any impact so we need to left multiply by the inverse.
|
||||
// This makes the final internal brush transform equal to `parent_transform`, allowing you to stretch a gradient by transforming the parent folder.
|
||||
let inverse_element_transform = (element_transform.matrix2.determinant() != 0.).then(|| element_transform.inverse()).unwrap_or_default();
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
|
||||
}
|
||||
Fill::None => {}
|
||||
};
|
||||
}
|
||||
Op::Stroke => {
|
||||
if let Some(stroke) = instance.instance.style.stroke() {
|
||||
let color = match stroke.color {
|
||||
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
|
||||
None => peniko::Color::TRANSPARENT,
|
||||
};
|
||||
let cap = match stroke.cap {
|
||||
StrokeCap::Butt => Cap::Butt,
|
||||
StrokeCap::Round => Cap::Round,
|
||||
StrokeCap::Square => Cap::Square,
|
||||
};
|
||||
let join = match stroke.join {
|
||||
StrokeJoin::Miter => Join::Miter,
|
||||
StrokeJoin::Bevel => Join::Bevel,
|
||||
StrokeJoin::Round => Join::Round,
|
||||
};
|
||||
let stroke = kurbo::Stroke {
|
||||
width: stroke.weight * if can_draw_aligned_stroke { 2. } else { 1. },
|
||||
miter_limit: stroke.join_miter_limit,
|
||||
join,
|
||||
start_cap: cap,
|
||||
end_cap: cap,
|
||||
dash_pattern: stroke.dash_lengths.into(),
|
||||
dash_offset: stroke.dash_offset,
|
||||
};
|
||||
|
||||
// Draw the stroke if it's visible
|
||||
if stroke.width > 0. {
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_draw_aligned_stroke {
|
||||
scene.pop_layer();
|
||||
scene.pop_layer();
|
||||
}
|
||||
|
||||
// If we pushed a layer for opacity or a blend mode, we need to pop it
|
||||
if layer {
|
||||
scene.pop_layer();
|
||||
|
@ -689,11 +893,11 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
|
||||
let stroke_width = instance.instance.style.stroke().map(|s| s.weight()).unwrap_or_default();
|
||||
|
||||
let miter_limit = instance.instance.style.stroke().map(|s| s.line_join_miter_limit).unwrap_or(1.);
|
||||
let miter_limit = instance.instance.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
|
||||
|
||||
let scale = transform.decompose_scale();
|
||||
|
||||
// We use the full line width here to account for different styles of line caps
|
||||
// We use the full line width here to account for different styles of stroke caps
|
||||
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
|
||||
|
||||
instance.instance.bounding_box_with_transform(transform * *instance.transform).map(|[a, b]| [a - offset, b + offset])
|
||||
|
@ -844,13 +1048,13 @@ impl GraphicElementRendered for Artboard {
|
|||
let color = peniko::Color::new([self.background.r(), self.background.g(), self.background.b(), self.background.a()]);
|
||||
let [a, b] = [self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()];
|
||||
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
|
||||
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 {
|
||||
let blend_mode = peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcOver);
|
||||
scene.push_layer(blend_mode, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
|
||||
}
|
||||
// Since the graphic group's transform is right multiplied in when rendering the graphic group, we just need to right multiply by the offset here.
|
||||
|
@ -935,9 +1139,9 @@ impl GraphicElementRendered for ArtboardGroupTable {
|
|||
}
|
||||
|
||||
impl GraphicElementRendered for RasterDataTable<CPU> {
|
||||
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
for instance in self.instance_ref_iter() {
|
||||
let transform = *instance.transform * render.transform;
|
||||
let transform = *instance.transform;
|
||||
|
||||
let image = &instance.instance;
|
||||
if image.data.is_empty() {
|
||||
|
@ -961,8 +1165,10 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
|
|||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
if instance.alpha_blending.opacity < 1. {
|
||||
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
|
||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
||||
let opacity = instance.alpha_blending.opacity * factor;
|
||||
if opacity < 1. {
|
||||
attributes.push("opacity", opacity.to_string());
|
||||
}
|
||||
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
attributes.push("style", instance.alpha_blending.blend_mode.render());
|
||||
|
|
|
@ -318,6 +318,32 @@ impl SetBlendMode for RasterDataTable<CPU> {
|
|||
}
|
||||
}
|
||||
|
||||
trait SetClip {
|
||||
fn set_clip(&mut self, clip: bool);
|
||||
}
|
||||
|
||||
impl SetClip for VectorDataTable {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetClip for GraphicGroupTable {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetClip for RasterDataTable<CPU> {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn blend_mode<T: SetBlendMode>(
|
||||
_: impl Ctx,
|
||||
|
@ -343,9 +369,31 @@ fn opacity<T: MultiplyAlpha>(
|
|||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
#[default(100.)] factor: Percentage,
|
||||
#[default(100.)] opacity: Percentage,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.multiply_alpha(factor / 100.);
|
||||
value.multiply_alpha(opacity / 100.);
|
||||
value
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
GraphicGroupTable,
|
||||
VectorDataTable,
|
||||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
blend_mode: BlendMode,
|
||||
#[default(100.)] opacity: Percentage,
|
||||
#[default(100.)] fill: Percentage,
|
||||
#[default(false)] clip: bool,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.set_blend_mode(blend_mode);
|
||||
value.multiply_alpha(opacity / 100.);
|
||||
value.multiply_fill(fill / 100.);
|
||||
value.set_clip(clip);
|
||||
value
|
||||
}
|
||||
|
|
|
@ -1321,6 +1321,36 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) trait MultiplyFill {
|
||||
fn multiply_fill(&mut self, factor: f64);
|
||||
}
|
||||
impl MultiplyFill for Color {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for VectorDataTable {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for GraphicGroupTable {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for RasterDataTable<CPU> {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aims for interoperable compatibility with:
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold
|
||||
//
|
||||
|
|
|
@ -514,6 +514,11 @@ impl Color {
|
|||
self.alpha
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
self.alpha > 1. - f32::EPSILON
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn average_rgb_channels(&self) -> f32 {
|
||||
(self.red + self.green + self.blue) / 3.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::Color;
|
||||
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
|
||||
use crate::renderer::format_transform_matrix;
|
||||
use crate::renderer::{RenderParams, format_transform_matrix};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::fmt::Write;
|
||||
|
@ -214,7 +214,7 @@ impl Gradient {
|
|||
}
|
||||
|
||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
|
||||
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], _render_params: &RenderParams) -> u64 {
|
||||
// TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to.
|
||||
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
|
@ -381,7 +381,7 @@ impl Fill {
|
|||
}
|
||||
|
||||
/// Renders the fill, adding necessary defs through mutating the first argument.
|
||||
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], render_params: &RenderParams) -> String {
|
||||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => {
|
||||
|
@ -392,7 +392,7 @@ impl Fill {
|
|||
result
|
||||
}
|
||||
Self::Gradient(gradient) => {
|
||||
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
format!(r##" fill="url('#{gradient_id}')""##)
|
||||
}
|
||||
}
|
||||
|
@ -413,6 +413,20 @@ impl Fill {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find if fill can be represented with only opaque colors
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Fill::Solid(color) => color.is_opaque(),
|
||||
Fill::Gradient(gradient) => gradient.stops.iter().all(|(_, color)| color.is_opaque()),
|
||||
Fill::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if fill is none
|
||||
pub fn is_none(&self) -> bool {
|
||||
*self == Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Fill {
|
||||
|
@ -499,19 +513,19 @@ pub enum FillType {
|
|||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum LineCap {
|
||||
pub enum StrokeCap {
|
||||
#[default]
|
||||
Butt,
|
||||
Round,
|
||||
Square,
|
||||
}
|
||||
|
||||
impl LineCap {
|
||||
impl StrokeCap {
|
||||
fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
LineCap::Butt => "butt",
|
||||
LineCap::Round => "round",
|
||||
LineCap::Square => "square",
|
||||
StrokeCap::Butt => "butt",
|
||||
StrokeCap::Round => "round",
|
||||
StrokeCap::Square => "square",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -519,29 +533,61 @@ impl LineCap {
|
|||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum LineJoin {
|
||||
pub enum StrokeJoin {
|
||||
#[default]
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
}
|
||||
|
||||
impl LineJoin {
|
||||
impl StrokeJoin {
|
||||
fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
LineJoin::Bevel => "bevel",
|
||||
LineJoin::Miter => "miter",
|
||||
LineJoin::Round => "round",
|
||||
StrokeJoin::Bevel => "bevel",
|
||||
StrokeJoin::Miter => "miter",
|
||||
StrokeJoin::Round => "round",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum StrokeAlign {
|
||||
#[default]
|
||||
Center,
|
||||
Inside,
|
||||
Outside,
|
||||
}
|
||||
|
||||
impl StrokeAlign {
|
||||
pub fn is_not_centered(self) -> bool {
|
||||
self != Self::Center
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum PaintOrder {
|
||||
#[default]
|
||||
StrokeAbove,
|
||||
StrokeBelow,
|
||||
}
|
||||
|
||||
impl PaintOrder {
|
||||
pub fn is_default(self) -> bool {
|
||||
self == Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn daffine2_identity() -> DAffine2 {
|
||||
DAffine2::IDENTITY
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct Stroke {
|
||||
/// Stroke color
|
||||
pub color: Option<Color>,
|
||||
|
@ -549,26 +595,38 @@ pub struct Stroke {
|
|||
pub weight: f64,
|
||||
pub dash_lengths: Vec<f64>,
|
||||
pub dash_offset: f64,
|
||||
pub line_cap: LineCap,
|
||||
pub line_join: LineJoin,
|
||||
pub line_join_miter_limit: f64,
|
||||
#[serde(alias = "line_cap")]
|
||||
pub cap: StrokeCap,
|
||||
#[serde(alias = "line_join")]
|
||||
pub join: StrokeJoin,
|
||||
#[serde(alias = "line_join_miter_limit")]
|
||||
pub join_miter_limit: f64,
|
||||
#[serde(default)]
|
||||
pub align: StrokeAlign,
|
||||
#[serde(default = "daffine2_identity")]
|
||||
pub transform: DAffine2,
|
||||
#[serde(default)]
|
||||
pub non_scaling: bool,
|
||||
#[serde(default)]
|
||||
pub paint_order: PaintOrder,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for Stroke {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.color.hash(state);
|
||||
self.weight.to_bits().hash(state);
|
||||
self.dash_lengths.len().hash(state);
|
||||
self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state));
|
||||
{
|
||||
self.dash_lengths.len().hash(state);
|
||||
self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state));
|
||||
}
|
||||
self.dash_offset.to_bits().hash(state);
|
||||
self.line_cap.hash(state);
|
||||
self.line_join.hash(state);
|
||||
self.line_join_miter_limit.to_bits().hash(state);
|
||||
self.cap.hash(state);
|
||||
self.join.hash(state);
|
||||
self.join_miter_limit.to_bits().hash(state);
|
||||
self.align.hash(state);
|
||||
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
self.non_scaling.hash(state);
|
||||
self.paint_order.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -590,11 +648,13 @@ impl Stroke {
|
|||
weight,
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
cap: StrokeCap::Butt,
|
||||
join: StrokeJoin::Miter,
|
||||
join_miter_limit: 4.,
|
||||
align: StrokeAlign::Center,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -604,14 +664,16 @@ impl Stroke {
|
|||
weight: self.weight + (other.weight - self.weight) * time,
|
||||
dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time).collect(),
|
||||
dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time,
|
||||
line_cap: if time < 0.5 { self.line_cap } else { other.line_cap },
|
||||
line_join: if time < 0.5 { self.line_join } else { other.line_join },
|
||||
line_join_miter_limit: self.line_join_miter_limit + (other.line_join_miter_limit - self.line_join_miter_limit) * time,
|
||||
cap: if time < 0.5 { self.cap } else { other.cap },
|
||||
join: if time < 0.5 { self.join } else { other.join },
|
||||
join_miter_limit: self.join_miter_limit + (other.join_miter_limit - self.join_miter_limit) * time,
|
||||
align: if time < 0.5 { self.align } else { other.align },
|
||||
transform: DAffine2::from_mat2_translation(
|
||||
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
|
||||
self.transform.translation * time + other.transform.translation * (1. - time),
|
||||
),
|
||||
non_scaling: if time < 0.5 { self.non_scaling } else { other.non_scaling },
|
||||
paint_order: if time < 0.5 { self.paint_order } else { other.paint_order },
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -637,23 +699,23 @@ impl Stroke {
|
|||
self.dash_offset
|
||||
}
|
||||
|
||||
pub fn line_cap_index(&self) -> u32 {
|
||||
self.line_cap as u32
|
||||
pub fn cap_index(&self) -> u32 {
|
||||
self.cap as u32
|
||||
}
|
||||
|
||||
pub fn line_join_index(&self) -> u32 {
|
||||
self.line_join as u32
|
||||
pub fn join_index(&self) -> u32 {
|
||||
self.join as u32
|
||||
}
|
||||
|
||||
pub fn line_join_miter_limit(&self) -> f32 {
|
||||
self.line_join_miter_limit as f32
|
||||
pub fn join_miter_limit(&self) -> f32 {
|
||||
self.join_miter_limit as f32
|
||||
}
|
||||
|
||||
/// Provide the SVG attributes for the stroke.
|
||||
pub fn render(&self) -> String {
|
||||
pub fn render(&self, aligned_strokes: bool, override_paint_order: bool, _render_params: &RenderParams) -> String {
|
||||
// Don't render a stroke at all if it would be invisible
|
||||
let Some(color) = self.color else { return String::new() };
|
||||
if self.weight <= 0. || color.a() == 0. {
|
||||
if !self.has_renderable_stroke() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
|
@ -661,16 +723,21 @@ impl Stroke {
|
|||
let weight = (self.weight != 1.).then_some(self.weight);
|
||||
let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths());
|
||||
let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset);
|
||||
let line_cap = (self.line_cap != LineCap::Butt).then_some(self.line_cap);
|
||||
let line_join = (self.line_join != LineJoin::Miter).then_some(self.line_join);
|
||||
let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit);
|
||||
let stroke_cap = (self.cap != StrokeCap::Butt).then_some(self.cap);
|
||||
let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join);
|
||||
let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit);
|
||||
let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align);
|
||||
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || override_paint_order).then_some(PaintOrder::StrokeBelow);
|
||||
|
||||
// Render the needed stroke attributes
|
||||
let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
if let Some(weight) = weight {
|
||||
if let Some(mut weight) = weight {
|
||||
if stroke_align.is_some() && aligned_strokes {
|
||||
weight *= 2.;
|
||||
}
|
||||
let _ = write!(&mut attributes, r#" stroke-width="{}""#, weight);
|
||||
}
|
||||
if let Some(dash_array) = dash_array {
|
||||
|
@ -679,19 +746,22 @@ impl Stroke {
|
|||
if let Some(dash_offset) = dash_offset {
|
||||
let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset);
|
||||
}
|
||||
if let Some(line_cap) = line_cap {
|
||||
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap.svg_name());
|
||||
if let Some(stroke_cap) = stroke_cap {
|
||||
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, stroke_cap.svg_name());
|
||||
}
|
||||
if let Some(line_join) = line_join {
|
||||
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join.svg_name());
|
||||
if let Some(stroke_join) = stroke_join {
|
||||
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, stroke_join.svg_name());
|
||||
}
|
||||
if let Some(line_join_miter_limit) = line_join_miter_limit {
|
||||
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit);
|
||||
if let Some(stroke_join_miter_limit) = stroke_join_miter_limit {
|
||||
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, stroke_join_miter_limit);
|
||||
}
|
||||
// Add vector-effect attribute to make strokes non-scaling
|
||||
if self.non_scaling {
|
||||
let _ = write!(&mut attributes, r#" vector-effect="non-scaling-stroke""#);
|
||||
}
|
||||
if paint_order.is_some() {
|
||||
let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#);
|
||||
}
|
||||
attributes
|
||||
}
|
||||
|
||||
|
@ -724,18 +794,23 @@ impl Stroke {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_line_cap(mut self, line_cap: LineCap) -> Self {
|
||||
self.line_cap = line_cap;
|
||||
pub fn with_stroke_cap(mut self, stroke_cap: StrokeCap) -> Self {
|
||||
self.cap = stroke_cap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_join(mut self, line_join: LineJoin) -> Self {
|
||||
self.line_join = line_join;
|
||||
pub fn with_stroke_join(mut self, stroke_join: StrokeJoin) -> Self {
|
||||
self.join = stroke_join;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_join_miter_limit(mut self, limit: f64) -> Self {
|
||||
self.line_join_miter_limit = limit;
|
||||
pub fn with_stroke_join_miter_limit(mut self, limit: f64) -> Self {
|
||||
self.join_miter_limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_align(mut self, stroke_align: StrokeAlign) -> Self {
|
||||
self.align = stroke_align;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -743,6 +818,10 @@ impl Stroke {
|
|||
self.non_scaling = non_scaling;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_renderable_stroke(&self) -> bool {
|
||||
self.weight > 0. && self.color.is_some_and(|color| color.a() != 0.)
|
||||
}
|
||||
}
|
||||
|
||||
// Having an alpha of 1 to start with leads to a better experience with the properties panel
|
||||
|
@ -753,11 +832,13 @@ impl Default for Stroke {
|
|||
color: Some(Color::from_rgba8_srgb(0, 0, 0, 255)),
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
cap: StrokeCap::Butt,
|
||||
join: StrokeJoin::Miter,
|
||||
join_miter_limit: 4.,
|
||||
align: StrokeAlign::Center,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order: PaintOrder::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -929,19 +1010,35 @@ impl PathStyle {
|
|||
}
|
||||
|
||||
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
|
||||
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render(
|
||||
&self,
|
||||
svg_defs: &mut String,
|
||||
element_transform: DAffine2,
|
||||
stroke_transform: DAffine2,
|
||||
bounds: [DVec2; 2],
|
||||
transformed_bounds: [DVec2; 2],
|
||||
aligned_strokes: bool,
|
||||
override_paint_order: bool,
|
||||
render_params: &RenderParams,
|
||||
) -> String {
|
||||
let view_mode = render_params.view_mode;
|
||||
match view_mode {
|
||||
ViewMode::Outline => {
|
||||
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT);
|
||||
// Outline strokes should be non-scaling by default
|
||||
outline_stroke.non_scaling = true;
|
||||
let stroke_attribute = outline_stroke.render();
|
||||
let stroke_attribute = outline_stroke.render(aligned_strokes, override_paint_order, render_params);
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
_ => {
|
||||
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default();
|
||||
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
let stroke_attribute = self
|
||||
.stroke
|
||||
.as_ref()
|
||||
.map(|stroke| stroke.render(aligned_strokes, override_paint_order, render_params))
|
||||
.unwrap_or_default();
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier,
|
|||
use crate::renderer::GraphicElementRendered;
|
||||
use crate::transform::{Footprint, ReferencePoint, Transform};
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use crate::vector::style::{LineCap, LineJoin};
|
||||
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use crate::vector::{FillId, PointDomain, RegionId};
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
|
||||
use bezier_rs::{Join, ManipulatorGroup, Subpath};
|
||||
|
@ -167,17 +167,22 @@ async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
|
|||
#[default(2.)]
|
||||
/// The stroke weight.
|
||||
weight: f64,
|
||||
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
|
||||
dash_lengths: Vec<f64>,
|
||||
/// The offset distance from the starting point of the dash pattern.
|
||||
dash_offset: f64,
|
||||
/// The alignment of stroke to the path's centerline or (for closed shapes) the inside or outside of the shape.
|
||||
align: StrokeAlign,
|
||||
/// The shape of the stroke at open endpoints.
|
||||
line_cap: crate::vector::style::LineCap,
|
||||
cap: StrokeCap,
|
||||
/// The curvature of the bent stroke at sharp corners.
|
||||
line_join: LineJoin,
|
||||
join: StrokeJoin,
|
||||
#[default(4.)]
|
||||
/// The threshold for when a miter-joined stroke is converted to a bevel-joined stroke when a sharp angle becomes pointier than this ratio.
|
||||
miter_limit: f64,
|
||||
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
|
||||
/// <https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty>
|
||||
paint_order: PaintOrder,
|
||||
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
|
||||
dash_lengths: Vec<f64>,
|
||||
/// The phase offset distance from the starting point of the dash pattern.
|
||||
dash_offset: f64,
|
||||
) -> Instances<V>
|
||||
where
|
||||
Instances<V>: VectorDataTableIterMut + 'n + Send,
|
||||
|
@ -187,12 +192,15 @@ where
|
|||
weight,
|
||||
dash_lengths,
|
||||
dash_offset,
|
||||
line_cap,
|
||||
line_join,
|
||||
line_join_miter_limit: miter_limit,
|
||||
cap,
|
||||
join,
|
||||
join_miter_limit: miter_limit,
|
||||
align,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order,
|
||||
};
|
||||
|
||||
for vector in vector_data.vector_iter_mut() {
|
||||
let mut stroke = stroke.clone();
|
||||
stroke.transform *= *vector.transform;
|
||||
|
@ -1084,7 +1092,7 @@ async fn points_to_polyline(_: impl Ctx, mut points: VectorDataTable, #[default(
|
|||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
|
||||
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, line_join: LineJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
|
||||
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
|
@ -1106,10 +1114,10 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, l
|
|||
let mut subpath_out = offset_subpath(
|
||||
&subpath,
|
||||
-distance,
|
||||
match line_join {
|
||||
LineJoin::Miter => Join::Miter(Some(miter_limit)),
|
||||
LineJoin::Bevel => Join::Bevel,
|
||||
LineJoin::Round => Join::Round,
|
||||
match join {
|
||||
StrokeJoin::Miter => Join::Miter(Some(miter_limit)),
|
||||
StrokeJoin::Bevel => Join::Bevel,
|
||||
StrokeJoin::Round => Join::Round,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -1139,19 +1147,19 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
|
|||
let mut result = VectorData::default();
|
||||
|
||||
// Taking the existing stroke data and passing it to kurbo::stroke to generate new fill paths.
|
||||
let join = match stroke.line_join {
|
||||
LineJoin::Miter => kurbo::Join::Miter,
|
||||
LineJoin::Bevel => kurbo::Join::Bevel,
|
||||
LineJoin::Round => kurbo::Join::Round,
|
||||
let join = match stroke.join {
|
||||
StrokeJoin::Miter => kurbo::Join::Miter,
|
||||
StrokeJoin::Bevel => kurbo::Join::Bevel,
|
||||
StrokeJoin::Round => kurbo::Join::Round,
|
||||
};
|
||||
let cap = match stroke.line_cap {
|
||||
LineCap::Butt => kurbo::Cap::Butt,
|
||||
LineCap::Round => kurbo::Cap::Round,
|
||||
LineCap::Square => kurbo::Cap::Square,
|
||||
let cap = match stroke.cap {
|
||||
StrokeCap::Butt => kurbo::Cap::Butt,
|
||||
StrokeCap::Round => kurbo::Cap::Round,
|
||||
StrokeCap::Square => kurbo::Cap::Square,
|
||||
};
|
||||
let dash_offset = stroke.dash_offset;
|
||||
let dash_pattern = stroke.dash_lengths;
|
||||
let miter_limit = stroke.line_join_miter_limit;
|
||||
let miter_limit = stroke.join_miter_limit;
|
||||
|
||||
let stroke_style = kurbo::Stroke::new(stroke.weight)
|
||||
.with_caps(cap)
|
||||
|
|
|
@ -234,8 +234,12 @@ tagged_value! {
|
|||
SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice),
|
||||
GridType(graphene_core::vector::misc::GridType),
|
||||
ArcType(graphene_core::vector::misc::ArcType),
|
||||
LineCap(graphene_core::vector::style::LineCap),
|
||||
LineJoin(graphene_core::vector::style::LineJoin),
|
||||
#[serde(alias = "LineCap")]
|
||||
StrokeCap(graphene_core::vector::style::StrokeCap),
|
||||
#[serde(alias = "LineJoin")]
|
||||
StrokeJoin(graphene_core::vector::style::StrokeJoin),
|
||||
StrokeAlign(graphene_core::vector::style::StrokeAlign),
|
||||
PaintOrder(graphene_core::vector::style::PaintOrder),
|
||||
FillType(graphene_core::vector::style::FillType),
|
||||
FillChoice(graphene_core::vector::style::FillChoice),
|
||||
GradientType(graphene_core::vector::style::GradientType),
|
||||
|
|
|
@ -259,7 +259,15 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
|
|||
ctx.footprint();
|
||||
|
||||
let RenderConfig { hide_artboards, for_export, .. } = render_config;
|
||||
let render_params = RenderParams::new(render_config.view_mode, None, false, hide_artboards, for_export);
|
||||
let render_params = RenderParams {
|
||||
view_mode: render_config.view_mode,
|
||||
culling_bounds: None,
|
||||
thumbnail: false,
|
||||
hide_artboards,
|
||||
for_export,
|
||||
for_mask: false,
|
||||
alignment_parent_transform: None,
|
||||
};
|
||||
|
||||
let data = data.eval(ctx.clone()).await;
|
||||
let editor_api = editor_api.eval(None).await;
|
||||
|
|
|
@ -61,8 +61,10 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::BooleanOperation]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<graphene_core::Color>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Fill]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::LineCap]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::LineJoin]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::StrokeCap]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::StrokeJoin]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::PaintOrder]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::StrokeAlign]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Stroke]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Gradient]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::GradientStops]),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue