Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
if let usvg::Paint::Color(color) = &stroke.paint() {
modify_inputs.stroke_set(Stroke {
color: Some(usvg_color(*color, stroke.opacity().get())),
//Added the gradient field to the Stroke struct
gradient: None,
weight: stroke.width().get() as f64,
dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
dash_offset: stroke.dashoffset() as f64,
Expand Down
20 changes: 15 additions & 5 deletions node-graph/libraries/rendering/src/render_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,23 @@ impl RenderExt for Stroke {
render_params: &RenderParams,
) -> Self::Output {
// Don't render a stroke at all if it would be invisible
let Some(color) = self.color else { return String::new() };
if !self.has_renderable_stroke() {
return String::new();
}
let paint = match (&self.gradient, self.color) {
(Some(gradient), _) => {
let gradient_id = gradient.render(_svg_defs, _element_transform, _stroke_transform, _bounds, _transformed_bounds, render_params);
format!(r##" stroke="url('#{gradient_id}')""##)
}
(_, Some(color)) => {
let mut result = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. {
let _ = write!(result, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
result
}
_ => return String::new(),
};

let default_weight = if self.align != StrokeAlign::Center && render_params.aligned_strokes { 1. / 2. } else { 1. };

Expand All @@ -125,10 +138,7 @@ impl RenderExt for Stroke {
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.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.);
}
let mut attributes = paint;
if let Some(mut weight) = weight {
if stroke_align.is_some() && render_params.aligned_strokes {
weight *= 2.;
Expand Down
84 changes: 70 additions & 14 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1115,34 +1115,90 @@ impl Render for Table<Vector> {
};

let do_stroke = |scene: &mut Scene, width_scale: f64| {
if let Some(stroke) = row.element.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 {
if let Some(stroke_style) = row.element.style.stroke() {
let cap = match stroke_style.cap {
StrokeCap::Butt => Cap::Butt,
StrokeCap::Round => Cap::Round,
StrokeCap::Square => Cap::Square,
};
let join = match stroke.join {
let join = match stroke_style.join {
StrokeJoin::Miter => Join::Miter,
StrokeJoin::Bevel => Join::Bevel,
StrokeJoin::Round => Join::Round,
};
let dash_pattern = stroke.dash_lengths.iter().map(|l| l.max(0.)).collect();
let stroke = kurbo::Stroke {
width: stroke.weight * width_scale,
miter_limit: stroke.join_miter_limit,
let dash_pattern = stroke_style.dash_lengths.iter().map(|l| l.max(0.)).collect();
let kurbo_stroke = kurbo::Stroke {
width: stroke_style.weight * width_scale,
miter_limit: stroke_style.join_miter_limit,
join,
start_cap: cap,
end_cap: cap,
dash_pattern,
dash_offset: stroke.dash_offset,
dash_offset: stroke_style.dash_offset,
};

if stroke.width > 0. {
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
if kurbo_stroke.width > 0. {
let (brush, brush_transform) = if let Some(gradient) = stroke_style.gradient.as_ref() {
let mut stops = peniko::ColorStops::new();
for (position, color, _) in gradient.stops.interpolated_samples() {
stops.push(peniko::ColorStop {
offset: position as f32,
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
});
}

let bounds = row.element.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);

let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
parent_transform.inverse()
} else {
Default::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 brush = peniko::Brush::Gradient(peniko::Gradient {
kind: match gradient.gradient_type {
GradientType::Linear => peniko::LinearGradientPosition {
start: to_point(start),
end: to_point(end),
}
.into(),
GradientType::Radial => {
let radius = start.distance(end);
peniko::RadialGradientPosition {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(start),
end_radius: radius as f32,
}
.into()
}
},
stops,
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
..Default::default()
});
let inverse_element_transform = if element_transform.matrix2.determinant() != 0. {
element_transform.inverse()
} else {
Default::default()
};
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());

(brush, Some(brush_transform))
} else {
let color = stroke_style
.color
.map(|color| peniko::Color::new([color.r(), color.g(), color.b(), color.a()]))
.unwrap_or(peniko::Color::TRANSPARENT);
(peniko::Brush::Solid(color), None)
};

scene.stroke(&kurbo_stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, brush_transform, &path);
}
}
};
Expand Down
44 changes: 42 additions & 2 deletions node-graph/libraries/vector-types/src/vector/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ fn daffine2_identity() -> DAffine2 {
pub struct Stroke {
/// Stroke color
pub color: Option<Color>,
/// Optional gradient paint. If set, overrides `color`.
pub gradient: Option<Gradient>,
/// Line thickness
pub weight: f64,
pub dash_lengths: Vec<f64>,
Expand All @@ -325,6 +327,7 @@ pub struct Stroke {
impl std::hash::Hash for Stroke {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.color.hash(state);
self.gradient.hash(state);
self.weight.to_bits().hash(state);
{
self.dash_lengths.len().hash(state);
Expand All @@ -344,6 +347,7 @@ impl Stroke {
pub const fn new(color: Option<Color>, weight: f64) -> Self {
Self {
color,
gradient: None,
weight,
dash_lengths: Vec::new(),
dash_offset: 0.,
Expand All @@ -359,6 +363,12 @@ impl Stroke {
pub fn lerp(&self, other: &Self, time: f64) -> Self {
Self {
color: self.color.map(|color| color.lerp(&other.color.unwrap_or(color), time as f32)),
gradient: match (&self.gradient, &other.gradient) {
(Some(a), Some(b)) => Some(a.lerp(b, time)),
(Some(a), None) if time < 0.5 => Some(a.clone()),
(None, Some(b)) if time >= 0.5 => Some(b.clone()),
_ => None,
},
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,
Expand Down Expand Up @@ -398,6 +408,10 @@ impl Stroke {
pub fn color(&self) -> Option<Color> {
self.color
}
/// Get the current stroke gradient.
pub fn gradient(&self) -> Option<&Gradient> {
self.gradient.as_ref()
}

/// Get the current stroke weight.
pub fn weight(&self) -> f64 {
Expand Down Expand Up @@ -440,9 +454,20 @@ impl Stroke {

pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
self.color = *color;
if color.is_some() {
self.gradient = None;
}

Some(self)
}
/// Set the stroke's gradient, replacing the color if necessary.
pub fn with_gradient(mut self, gradient: Option<Gradient>) -> Self {
self.gradient = gradient;
if self.gradient.is_some() {
self.color = None;
}
self
}

pub fn with_weight(mut self, weight: f64) -> Self {
self.weight = weight;
Expand Down Expand Up @@ -488,7 +513,14 @@ impl Stroke {
}

pub fn has_renderable_stroke(&self) -> bool {
self.weight > 0. && self.color.is_some_and(|color| color.a() != 0.)
if self.weight <= 0. {
return false;
}

let has_color_alpha = self.color.is_some_and(|color| color.a() != 0.);
let has_gradient_alpha = self.gradient.as_ref().is_some_and(|gradient| gradient.stops.color.iter().any(|color| color.a() != 0.));

has_color_alpha || has_gradient_alpha
}
}

Expand All @@ -498,6 +530,7 @@ impl Default for Stroke {
Self {
weight: 0.,
color: Some(Color::from_rgba8_srgb(0, 0, 0, 255)),
gradient: None,
dash_lengths: Vec::new(),
dash_offset: 0.,
cap: StrokeCap::Butt,
Expand Down Expand Up @@ -530,7 +563,14 @@ impl std::fmt::Display for PathStyle {
let fill = &self.fill;

let stroke = match &self.stroke {
Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| c.to_rgba_hex_srgb()), stroke.weight),
Some(stroke) => {
let paint = match (&stroke.gradient, stroke.color) {
(Some(_), _) => "Gradient".to_string(),
(_, Some(color)) => format!("#{}", color.to_rgba_hex_srgb()),
_ => "None".to_string(),
};
format!("{paint} (Weight: {} px)", stroke.weight)
}
None => "None".to_string(),
};

Expand Down
1 change: 1 addition & 0 deletions node-graph/nodes/vector/src/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ where
{
let stroke = Stroke {
color: color.into(),
gradient: None,
weight,
dash_lengths: dash_lengths.into_vec(),
dash_offset,
Expand Down