Skip to content

Commit 4c876fd

Browse files
committed
feat: implemented stroke gradient feature
1 parent b1933e3 commit 4c876fd

File tree

8 files changed

+414
-70
lines changed

8 files changed

+414
-70
lines changed

editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ impl TableRowLayout for Vector {
389389
}
390390

391391
if let Some(stroke) = self.style.stroke.clone() {
392-
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
392+
let color = if let Some(color) = stroke.color() { FillChoice::Solid(color) } else { FillChoice::None };
393393
table_rows.push(vec![
394394
TextLabel::new("Stroke").narrow(true).widget_instance(),
395395
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_instance(),

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ fn import_usvg_node(
496496
fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
497497
if let usvg::Paint::Color(color) = &stroke.paint() {
498498
modify_inputs.stroke_set(Stroke {
499-
color: Some(usvg_color(*color, stroke.opacity().get())),
499+
paint: Fill::Solid(usvg_color(*color, stroke.opacity().get())),
500500
weight: stroke.width().get() as f64,
501501
dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
502502
dash_offset: stroke.dashoffset() as f64,

editor/src/messages/portfolio/document/graph_operation/utility_types.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,23 @@ impl<'a> ModifyInputsContext<'a> {
401401
return;
402402
};
403403

404-
let stroke_color = if let Some(color) = stroke.color { Table::new_from_element(color) } else { Table::new() };
404+
match &stroke.paint {
405+
Fill::None => {
406+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::BackupColorInput::INDEX);
407+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new()), false), true);
408+
}
409+
Fill::Solid(color) => {
410+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::BackupColorInput::INDEX);
411+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new_from_element(*color)), false), true);
412+
}
413+
Fill::Gradient(gradient) => {
414+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::BackupGradientInput::INDEX);
415+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true);
416+
}
417+
}
405418

406-
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
407-
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_color), false), true);
419+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::<Fill>::INDEX);
420+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(stroke.paint.clone()), false), true);
408421
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
409422
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
410423
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 194 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use graphene_std::text::{Font, TextAlign};
2727
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
2828
use graphene_std::vector::QRCodeErrorCorrectionLevel;
2929
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
30-
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
30+
use graphene_std::vector::style::{Fill, FillChoice, FillType, Gradient, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
3131

3232
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
3333
let widget = TextLabel::new(text).widget_instance();
@@ -2009,44 +2009,207 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
20092009
Some(TaggedValue::StrokeJoin(x)) => x,
20102010
_ => &StrokeJoin::Miter,
20112011
};
2012-
20132012
let dash_lengths_val = match &document_node.inputs[DashLengthsInput::<Vec<f64>>::INDEX].as_value() {
20142013
Some(TaggedValue::VecF64(x)) => x,
20152014
_ => &vec![],
20162015
};
2016+
20172017
let has_dash_lengths = dash_lengths_val.is_empty();
20182018
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
20192019

2020-
let color = color_widget(
2021-
ParameterWidgetsInfo::new(node_id, ColorInput::INDEX, true, context),
2022-
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
2020+
let fill = document_node
2021+
.inputs
2022+
.get(ColorInput::<Fill>::INDEX)
2023+
.and_then(|i| i.as_non_exposed_value())
2024+
.and_then(|t| match t {
2025+
TaggedValue::Fill(f) => Some(f.clone()),
2026+
_ => None,
2027+
})
2028+
.unwrap_or(Fill::None);
2029+
let backup_color = document_node
2030+
.inputs
2031+
.get(BackupColorInput::INDEX)
2032+
.and_then(|i| i.as_non_exposed_value())
2033+
.and_then(|t| match t {
2034+
TaggedValue::Color(c) => Some(c.clone()),
2035+
_ => None,
2036+
})
2037+
.unwrap_or(Table::new());
2038+
let backup_gradient = document_node
2039+
.inputs
2040+
.get(BackupGradientInput::INDEX)
2041+
.and_then(|i| i.as_non_exposed_value())
2042+
.and_then(|t| match t {
2043+
TaggedValue::Gradient(g) => Some(g.clone()),
2044+
_ => None,
2045+
})
2046+
.unwrap_or(Gradient::default());
2047+
2048+
let mut widgets = Vec::new();
2049+
2050+
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, ColorInput::<Fill>::INDEX, true, context));
2051+
let fill2 = fill.clone();
2052+
let backup_color_fill: Fill = backup_color.clone().into();
2053+
let backup_gradient_fill: Fill = backup_gradient.clone().into();
2054+
2055+
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
2056+
widgets_first_row.push(
2057+
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default()
2058+
.value(fill.clone().into())
2059+
.on_update(move |x: &crate::messages::layout::utility_types::widgets::button_widgets::ColorInput| Message::Batched {
2060+
messages: Box::new([
2061+
match &fill2 {
2062+
Fill::None => NodeGraphMessage::SetInputValue {
2063+
node_id,
2064+
input_index: BackupColorInput::INDEX,
2065+
value: TaggedValue::Color(Table::new()),
2066+
}
2067+
.into(),
2068+
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
2069+
node_id,
2070+
input_index: BackupColorInput::INDEX,
2071+
value: TaggedValue::Color(Table::new_from_element(*color)),
2072+
}
2073+
.into(),
2074+
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
2075+
node_id,
2076+
input_index: BackupGradientInput::INDEX,
2077+
value: TaggedValue::Gradient(gradient.clone()),
2078+
}
2079+
.into(),
2080+
},
2081+
NodeGraphMessage::SetInputValue {
2082+
node_id,
2083+
input_index: ColorInput::<Fill>::INDEX,
2084+
value: TaggedValue::Fill(x.value.to_fill(fill2.as_gradient())),
2085+
}
2086+
.into(),
2087+
]),
2088+
})
2089+
.on_commit(commit_value)
2090+
.widget_instance(),
20232091
);
2092+
widgets.push(LayoutGroup::row(widgets_first_row));
2093+
2094+
let mut fill_type_row = vec![TextLabel::new("").widget_instance()];
2095+
match fill {
2096+
Fill::Solid(_) | Fill::None => add_blank_assist(&mut fill_type_row),
2097+
Fill::Gradient(ref gradient) => {
2098+
let reverse_button = IconButton::new("Reverse", 24)
2099+
.tooltip_description("Reverse the gradient color stops.")
2100+
.on_update(update_value(
2101+
{
2102+
let gradient = gradient.clone();
2103+
move |_| {
2104+
let mut gradient = gradient.clone();
2105+
gradient.stops = gradient.stops.reversed();
2106+
TaggedValue::Fill(Fill::Gradient(gradient))
2107+
}
2108+
},
2109+
node_id,
2110+
ColorInput::<Fill>::INDEX,
2111+
))
2112+
.widget_instance();
2113+
fill_type_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
2114+
fill_type_row.push(reverse_button);
2115+
}
2116+
}
2117+
2118+
let entries = vec![
2119+
RadioEntryData::new("solid")
2120+
.label("Solid")
2121+
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, ColorInput::<Fill>::INDEX))
2122+
.on_commit(commit_value),
2123+
RadioEntryData::new("gradient")
2124+
.label("Gradient")
2125+
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, ColorInput::<Fill>::INDEX))
2126+
.on_commit(commit_value),
2127+
];
2128+
2129+
fill_type_row.extend_from_slice(&[
2130+
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
2131+
RadioInput::new(entries).selected_index(Some(if fill.as_gradient().is_some() { 1 } else { 0 })).widget_instance(),
2132+
]);
2133+
widgets.push(LayoutGroup::row(fill_type_row));
2134+
2135+
if let Fill::Gradient(gradient) = fill.clone() {
2136+
let mut row = vec![TextLabel::new("").widget_instance()];
2137+
match gradient.gradient_type {
2138+
GradientType::Linear => add_blank_assist(&mut row),
2139+
GradientType::Radial => {
2140+
let orientation = if (gradient.end.x - gradient.start.x).abs() > f64::EPSILON * 1e6 {
2141+
gradient.end.x > gradient.start.x
2142+
} else {
2143+
(gradient.start.x + gradient.start.y) < (gradient.end.x + gradient.end.y)
2144+
};
2145+
let reverse_radial_gradient_button = IconButton::new(if orientation { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
2146+
.tooltip_description("Reverse which end the gradient radiates from.")
2147+
.on_update(update_value(
2148+
{
2149+
let gradient = gradient.clone();
2150+
move |_| {
2151+
let mut gradient = gradient.clone();
2152+
std::mem::swap(&mut gradient.start, &mut gradient.end);
2153+
TaggedValue::Fill(Fill::Gradient(gradient))
2154+
}
2155+
},
2156+
node_id,
2157+
ColorInput::<Fill>::INDEX,
2158+
))
2159+
.widget_instance();
2160+
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
2161+
row.push(reverse_radial_gradient_button);
2162+
}
2163+
}
2164+
2165+
let gradient_for_closure = gradient.clone();
2166+
2167+
let entries = [GradientType::Linear, GradientType::Radial]
2168+
.iter()
2169+
.map(|&grad_type| {
2170+
let gradient = gradient_for_closure.clone();
2171+
let set_input_value = update_value(
2172+
move |_: &()| {
2173+
let mut new_gradient = gradient.clone();
2174+
new_gradient.gradient_type = grad_type;
2175+
TaggedValue::Fill(Fill::Gradient(new_gradient))
2176+
},
2177+
node_id,
2178+
ColorInput::<Fill>::INDEX,
2179+
);
2180+
RadioEntryData::new(format!("{:?}", grad_type))
2181+
.label(format!("{:?}", grad_type))
2182+
.on_update(move |_| Message::Batched {
2183+
messages: Box::new([
2184+
set_input_value(&()),
2185+
GradientToolMessage::UpdateOptions {
2186+
options: GradientOptionsUpdate::Type(grad_type),
2187+
}
2188+
.into(),
2189+
]),
2190+
})
2191+
.on_commit(commit_value)
2192+
})
2193+
.collect();
2194+
2195+
row.extend_from_slice(&[
2196+
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
2197+
RadioInput::new(entries).selected_index(Some(gradient.gradient_type as u32)).widget_instance(),
2198+
]);
2199+
2200+
widgets.push(LayoutGroup::row(row));
2201+
}
2202+
20242203
let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.));
2025-
let align = enum_choice::<StrokeAlign>()
2026-
.for_socket(ParameterWidgetsInfo::new(node_id, AlignInput::INDEX, true, context))
2027-
.property_row();
2204+
let align = enum_choice::<StrokeAlign>().for_socket(ParameterWidgetsInfo::new(node_id, AlignInput::INDEX, true, context)).property_row();
20282205
let cap = enum_choice::<StrokeCap>().for_socket(ParameterWidgetsInfo::new(node_id, CapInput::INDEX, true, context)).property_row();
2029-
let join = enum_choice::<StrokeJoin>()
2030-
.for_socket(ParameterWidgetsInfo::new(node_id, JoinInput::INDEX, true, context))
2031-
.property_row();
2032-
2033-
let miter_limit = number_widget(
2034-
ParameterWidgetsInfo::new(node_id, MiterLimitInput::INDEX, true, context),
2035-
NumberInput::default().min(0.).disabled(miter_limit_disabled),
2036-
);
2037-
let paint_order = enum_choice::<PaintOrder>()
2038-
.for_socket(ParameterWidgetsInfo::new(node_id, PaintOrderInput::INDEX, true, context))
2039-
.property_row();
2040-
let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths);
2041-
let dash_lengths = array_of_number_widget(
2042-
ParameterWidgetsInfo::new(node_id, DashLengthsInput::<Vec<f64>>::INDEX, true, context),
2043-
TextInput::default().centered(true),
2044-
);
2045-
let number_input = disabled_number_input;
2046-
let dash_offset = number_widget(ParameterWidgetsInfo::new(node_id, DashOffsetInput::INDEX, true, context), number_input);
2206+
let join = enum_choice::<StrokeJoin>().for_socket(ParameterWidgetsInfo::new(node_id, JoinInput::INDEX, true, context)).property_row();
2207+
let miter_limit = number_widget(ParameterWidgetsInfo::new(node_id, MiterLimitInput::INDEX, true, context), NumberInput::default().min(0.).disabled(miter_limit_disabled));
2208+
let paint_order = enum_choice::<PaintOrder>().for_socket(ParameterWidgetsInfo::new(node_id, PaintOrderInput::INDEX, true, context)).property_row();
2209+
let dash_lengths = array_of_number_widget(ParameterWidgetsInfo::new(node_id, DashLengthsInput::<Vec<f64>>::INDEX, true, context), TextInput::default().centered(true));
2210+
let dash_offset = number_widget(ParameterWidgetsInfo::new(node_id, DashOffsetInput::INDEX, true, context), NumberInput::default().unit(" px").disabled(has_dash_lengths));
20472211

2048-
vec![
2049-
color,
2212+
widgets.extend([
20502213
LayoutGroup::row(weight),
20512214
align,
20522215
cap,
@@ -2055,7 +2218,9 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
20552218
paint_order,
20562219
LayoutGroup::row(dash_lengths),
20572220
LayoutGroup::row(dash_offset),
2058-
]
2221+
]);
2222+
2223+
widgets
20592224
}
20602225

20612226
pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {

node-graph/libraries/rendering/src/render_ext.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,13 @@ impl RenderExt for Stroke {
9999
/// Provide the SVG attributes for the stroke.
100100
fn render(
101101
&self,
102-
_svg_defs: &mut String,
103-
_element_transform: DAffine2,
104-
_stroke_transform: DAffine2,
105-
_bounds: DAffine2,
102+
svg_defs: &mut String,
103+
element_transform: DAffine2,
104+
stroke_transform: DAffine2,
105+
bounds: DAffine2,
106106
_transformed_bounds: DAffine2,
107107
render_params: &RenderParams,
108108
) -> Self::Output {
109-
// Don't render a stroke at all if it would be invisible
110-
let Some(color) = self.color else { return String::new() };
111109
if !self.has_renderable_stroke() {
112110
return String::new();
113111
}
@@ -123,10 +121,21 @@ impl RenderExt for Stroke {
123121
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow);
124122

125123
// Render the needed stroke attributes
126-
let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
127-
if color.a() < 1. {
128-
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
129-
}
124+
let mut attributes = match &self.paint {
125+
Fill::None => return String::new(),
126+
Fill::Solid(color) => {
127+
let mut result = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
128+
if color.a() < 1. {
129+
let _ = write!(result, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
130+
}
131+
result
132+
}
133+
Fill::Gradient(gradient) => {
134+
let gradient_id = gradient.render(svg_defs, element_transform, stroke_transform, bounds, _transformed_bounds, render_params);
135+
format!(r##" stroke="url('#{gradient_id}')""##)
136+
}
137+
};
138+
130139
if let Some(mut weight) = weight {
131140
if stroke_align.is_some() && render_params.aligned_strokes {
132141
weight *= 2.;

0 commit comments

Comments
 (0)