diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 03fc166dd4..7be44a94ea 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -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, diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index d7736f804b..2de73ddc41 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -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. }; @@ -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.; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index c589e6177d..f9a42d03e3 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -1115,34 +1115,90 @@ impl Render for Table { }; 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); } } }; diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index 0828c4e6f2..ce096e7ad1 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -304,6 +304,8 @@ fn daffine2_identity() -> DAffine2 { pub struct Stroke { /// Stroke color pub color: Option, + /// Optional gradient paint. If set, overrides `color`. + pub gradient: Option, /// Line thickness pub weight: f64, pub dash_lengths: Vec, @@ -325,6 +327,7 @@ pub struct Stroke { impl std::hash::Hash for Stroke { fn hash(&self, state: &mut H) { self.color.hash(state); + self.gradient.hash(state); self.weight.to_bits().hash(state); { self.dash_lengths.len().hash(state); @@ -344,6 +347,7 @@ impl Stroke { pub const fn new(color: Option, weight: f64) -> Self { Self { color, + gradient: None, weight, dash_lengths: Vec::new(), dash_offset: 0., @@ -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, @@ -398,6 +408,10 @@ impl Stroke { pub fn color(&self) -> Option { 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 { @@ -440,9 +454,20 @@ impl Stroke { pub fn with_color(mut self, color: &Option) -> Option { 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) -> 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; @@ -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 } } @@ -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, @@ -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(), }; diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index f33d1a17fc..aea254524b 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -204,6 +204,7 @@ where { let stroke = Stroke { color: color.into(), + gradient: None, weight, dash_lengths: dash_lengths.into_vec(), dash_offset,