Skip to content

Commit d982683

Browse files
YohYamasakiKeavon
authored andcommitted
Add spread method support for gradients
* Add GradientSpreadMethod enum (Pad, Repeat, Reflect) to vector-types * Add radio buttons to gradient tool and fill properties panel * Convert spread method when importing SVGs via usvg * Sync backup gradient input when changing spread method * Table<GradientStops> rendering is not yet updated for spread method
1 parent da45ab2 commit d982683

6 files changed

Lines changed: 207 additions & 14 deletions

File tree

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use graphene_std::renderer::Quad;
1515
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
1616
use graphene_std::table::Table;
1717
use graphene_std::text::{Font, TypesettingConfig};
18-
use graphene_std::vector::style::{Fill, Gradient, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
18+
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
1919

2020
#[derive(ExtractField)]
2121
pub struct GraphOperationMessageContext<'a> {
@@ -765,6 +765,14 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
765765
}
766766
}
767767

768+
fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMethod {
769+
match spread_method {
770+
usvg::SpreadMethod::Pad => GradientSpreadMethod::Pad,
771+
usvg::SpreadMethod::Reflect => GradientSpreadMethod::Reflect,
772+
usvg::SpreadMethod::Repeat => GradientSpreadMethod::Repeat,
773+
}
774+
}
775+
768776
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap<String, GradientStops>) {
769777
modify_inputs.fill_set(match &fill.paint() {
770778
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())),
@@ -787,8 +795,15 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
787795
GradientStops::new(stops)
788796
}
789797
};
790-
791-
Fill::Gradient(Gradient { start, end, gradient_type, stops })
798+
let spread_method = convert_spread_method(linear.spread_method());
799+
800+
Fill::Gradient(Gradient {
801+
start,
802+
end,
803+
gradient_type,
804+
stops,
805+
spread_method,
806+
})
792807
}
793808
usvg::Paint::RadialGradient(radial) => {
794809
let gradient_transform = usvg_transform(radial.transform());
@@ -810,8 +825,15 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
810825
GradientStops::new(stops)
811826
}
812827
};
813-
814-
Fill::Gradient(Gradient { start, end, gradient_type, stops })
828+
let spread_method = convert_spread_method(radial.spread_method());
829+
830+
Fill::Gradient(Gradient {
831+
start,
832+
end,
833+
gradient_type,
834+
stops,
835+
spread_method,
836+
})
815837
}
816838
usvg::Paint::Pattern(_) => {
817839
warn!("SVG patterns are not currently supported");

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
2727
use graphene_std::vector::QRCodeErrorCorrectionLevel;
2828
use graphene_std::vector::misc::BooleanOperation;
2929
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, 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, GradientSpreadMethod, 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();
@@ -2006,6 +2006,54 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
20062006
]);
20072007

20082008
widgets.push(LayoutGroup::row(row));
2009+
2010+
let mut spread_methods_row: Vec<WidgetInstance> = vec![];
2011+
2012+
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Repeat, GradientSpreadMethod::Reflect]
2013+
.iter()
2014+
.map(|&spread_method| {
2015+
let gradient_for_input = gradient_for_closure.clone();
2016+
let gradient_for_backup = gradient_for_closure.clone();
2017+
2018+
let set_input_value = update_value(
2019+
move |_: &()| {
2020+
let mut new_gradient = gradient_for_input.clone();
2021+
new_gradient.spread_method = spread_method;
2022+
TaggedValue::Fill(Fill::Gradient(new_gradient))
2023+
},
2024+
node_id,
2025+
FillInput::<Color>::INDEX,
2026+
);
2027+
2028+
let set_backup_value = update_value(
2029+
move |_: &()| {
2030+
let mut new_gradient = gradient_for_backup.clone();
2031+
new_gradient.spread_method = spread_method;
2032+
TaggedValue::Gradient(new_gradient)
2033+
},
2034+
node_id,
2035+
BackupGradientInput::INDEX,
2036+
);
2037+
2038+
RadioEntryData::new(format!("{:?}", spread_method))
2039+
.label(format!("{:?}", spread_method))
2040+
.on_update(move |_| Message::Batched {
2041+
messages: Box::new([
2042+
set_input_value(&()),
2043+
set_backup_value(&()),
2044+
GradientToolMessage::UpdateOptions {
2045+
options: GradientOptionsUpdate::SpreadMethod(spread_method),
2046+
}
2047+
.into(),
2048+
]),
2049+
})
2050+
.on_commit(commit_value)
2051+
})
2052+
.collect();
2053+
2054+
spread_methods_row.extend_from_slice(&[RadioInput::new(spread_method_entries).selected_index(Some(gradient.spread_method as u32)).widget_instance()]);
2055+
2056+
widgets.push(LayoutGroup::row(spread_methods_row));
20092057
}
20102058

20112059
widgets

editor/src/messages/tool/tool_messages/gradient_tool.rs

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
99
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient};
1010
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
1111
use graphene_std::raster::color::Color;
12-
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType};
12+
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStops, GradientType};
1313

1414
#[derive(Default, ExtractField)]
1515
pub struct GradientTool {
@@ -21,6 +21,7 @@ pub struct GradientTool {
2121
#[derive(Default)]
2222
pub struct GradientOptions {
2323
gradient_type: GradientType,
24+
spread_method: GradientSpreadMethod,
2425
}
2526

2627
#[impl_message(Message, ToolMessage, Gradient)]
@@ -53,6 +54,7 @@ pub enum GradientOptionsUpdate {
5354
Type(GradientType),
5455
ReverseStops,
5556
ReverseDirection,
57+
SpreadMethod(GradientSpreadMethod),
5658
}
5759

5860
impl ToolMetadata for GradientTool {
@@ -84,6 +86,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
8486
GradientOptionsUpdate::ReverseDirection => {
8587
apply_gradient_update(&mut self.data, context, responses, |_| true, |g| std::mem::swap(&mut g.start, &mut g.end));
8688
}
89+
GradientOptionsUpdate::SpreadMethod(spread_method) => {
90+
self.options.spread_method = spread_method;
91+
apply_gradient_update(&mut self.data, context, responses, |g| g.spread_method != spread_method, |g| g.spread_method = spread_method);
92+
}
8793
},
8894
ToolMessage::Gradient(GradientToolMessage::StartTransactionForColorStop) => {
8995
if self.data.color_picker_transaction_open {
@@ -168,7 +174,36 @@ impl LayoutHolder for GradientTool {
168174
})
169175
.widget_instance();
170176

171-
let mut widgets = vec![gradient_type, Separator::new(SeparatorStyle::Unrelated).widget_instance(), reverse_stops];
177+
let spread_method = RadioInput::new(vec![
178+
RadioEntryData::new("Pad").label("Pad").tooltip_label("Pad").on_update(move |_| {
179+
GradientToolMessage::UpdateOptions {
180+
options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Pad),
181+
}
182+
.into()
183+
}),
184+
RadioEntryData::new("Repeat").label("Repeat").tooltip_label("Repeat").on_update(move |_| {
185+
GradientToolMessage::UpdateOptions {
186+
options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Repeat),
187+
}
188+
.into()
189+
}),
190+
RadioEntryData::new("Reflect").label("Reflect").tooltip_label("Reflect").on_update(move |_| {
191+
GradientToolMessage::UpdateOptions {
192+
options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Reflect),
193+
}
194+
.into()
195+
}),
196+
])
197+
.selected_index(Some(self.options.spread_method as u32))
198+
.widget_instance();
199+
200+
let mut widgets = vec![
201+
gradient_type,
202+
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
203+
reverse_stops,
204+
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
205+
spread_method,
206+
];
172207

173208
if self.options.gradient_type == GradientType::Radial {
174209
let orientation = self
@@ -1149,7 +1184,14 @@ impl Fsm for GradientToolFsmState {
11491184
gradient.clone()
11501185
} else {
11511186
// Generate a new gradient
1152-
Gradient::new(DVec2::ZERO, global_tool_data.secondary_color, DVec2::ONE, global_tool_data.primary_color, tool_options.gradient_type)
1187+
Gradient::new(
1188+
DVec2::ZERO,
1189+
global_tool_data.secondary_color,
1190+
DVec2::ONE,
1191+
global_tool_data.primary_color,
1192+
tool_options.gradient_type,
1193+
tool_options.spread_method,
1194+
)
11531195
};
11541196
let mut selected_gradient = SelectedGradient::new(gradient, layer, document);
11551197
selected_gradient.dragging = GradientDragTarget::New;
@@ -1941,4 +1983,38 @@ mod test_gradient {
19411983
// Additional verification that 0.75 stop is gone
19421984
assert!(!final_positions.iter().any(|pos| (pos - 0.75).abs() < 0.05), "Stop at position 0.75 should have been deleted");
19431985
}
1986+
1987+
#[tokio::test]
1988+
async fn change_spread_method() {
1989+
use graphene_std::vector::style::GradientSpreadMethod;
1990+
1991+
let mut editor = EditorTestUtils::create();
1992+
editor.new_document().await;
1993+
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
1994+
editor.drag_tool(ToolType::Gradient, 10., 10., 90., 90., ModifierKeys::empty()).await;
1995+
1996+
// Verify default spread method is Pad
1997+
let (gradient, _) = get_gradient(&mut editor).await;
1998+
assert_eq!(gradient.spread_method, GradientSpreadMethod::Pad);
1999+
2000+
// Update spread method to Repeat
2001+
editor
2002+
.handle_message(GradientToolMessage::UpdateOptions {
2003+
options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Repeat),
2004+
})
2005+
.await;
2006+
2007+
let (gradient, _) = get_gradient(&mut editor).await;
2008+
assert_eq!(gradient.spread_method, GradientSpreadMethod::Repeat);
2009+
2010+
// Update spread method to Reflect
2011+
editor
2012+
.handle_message(GradientToolMessage::UpdateOptions {
2013+
options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Reflect),
2014+
})
2015+
.await;
2016+
2017+
let (gradient, _) = get_gradient(&mut editor).await;
2018+
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
2019+
}
19442020
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,24 @@ impl RenderExt for Gradient {
4747
format!(r#" gradientTransform="{gradient_transform}""#)
4848
};
4949

50+
let spread_method = self.spread_method.svg_name();
51+
let spread_method = if spread_method.is_empty() { String::new() } else { format!(r#" spreadMethod="{spread_method}""#) };
52+
5053
let gradient_id = generate_uuid();
5154

5255
match self.gradient_type {
5356
GradientType::Linear => {
5457
let _ = write!(
5558
svg_defs,
56-
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{gradient_transform}>{}</linearGradient>"#,
59+
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{spread_method}{gradient_transform}>{}</linearGradient>"#,
5760
gradient_id, start.x, start.y, end.x, end.y, stop
5861
);
5962
}
6063
GradientType::Radial => {
6164
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
6265
let _ = write!(
6366
svg_defs,
64-
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{gradient_transform}>{}</radialGradient>"#,
67+
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{spread_method}{gradient_transform}>{}</radialGradient>"#,
6568
gradient_id, start.x, start.y, radius, stop
6669
);
6770
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use std::fmt::Write;
2424
use std::hash::{Hash, Hasher};
2525
use std::ops::Deref;
2626
use std::sync::{Arc, LazyLock};
27+
use vector_types::gradient::GradientSpreadMethod;
2728
use vello::*;
2829

2930
/// Cached 16x16 transparency checkerboard image data (two 8x8 cells of #ffffff and #cccccc).
@@ -1159,6 +1160,11 @@ impl Render for Table<Vector> {
11591160
.into()
11601161
}
11611162
},
1163+
extend: match gradient.spread_method {
1164+
GradientSpreadMethod::Pad => peniko::Extend::Pad,
1165+
GradientSpreadMethod::Reflect => peniko::Extend::Reflect,
1166+
GradientSpreadMethod::Repeat => peniko::Extend::Repeat,
1167+
},
11621168
stops,
11631169
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
11641170
..Default::default()

node-graph/libraries/vector-types/src/gradient.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,27 @@ impl GradientStops {
334334
}
335335
}
336336

337+
#[repr(C)]
338+
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
339+
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)]
340+
#[widget(Radio)]
341+
pub enum GradientSpreadMethod {
342+
#[default]
343+
Pad,
344+
Repeat,
345+
Reflect,
346+
}
347+
348+
impl GradientSpreadMethod {
349+
pub fn svg_name(&self) -> &'static str {
350+
match self {
351+
GradientSpreadMethod::Pad => "",
352+
GradientSpreadMethod::Reflect => "reflect",
353+
GradientSpreadMethod::Repeat => "repeat",
354+
}
355+
}
356+
}
357+
337358
/// A gradient fill.
338359
///
339360
/// Contains the start and end points, along with the colors at varying points along the length.
@@ -345,6 +366,8 @@ pub struct Gradient {
345366
pub gradient_type: GradientType,
346367
pub start: DVec2,
347368
pub end: DVec2,
369+
#[serde(default)]
370+
pub spread_method: GradientSpreadMethod,
348371
}
349372

350373
impl Default for Gradient {
@@ -354,6 +377,7 @@ impl Default for Gradient {
354377
gradient_type: GradientType::Linear,
355378
start: DVec2::new(0., 0.5),
356379
end: DVec2::new(1., 0.5),
380+
spread_method: GradientSpreadMethod::Pad,
357381
}
358382
}
359383
}
@@ -369,6 +393,7 @@ impl std::hash::Hash for Gradient {
369393
.for_each(|x| x.to_bits().hash(state));
370394
self.stops.color.iter().for_each(|color| color.hash(state));
371395
self.gradient_type.hash(state);
396+
self.spread_method.hash(state);
372397
}
373398
}
374399

@@ -387,7 +412,7 @@ impl std::fmt::Display for Gradient {
387412

388413
impl Gradient {
389414
/// Constructs a new gradient with the colors at 0 and 1 specified.
390-
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType) -> Self {
415+
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType, spread_method: GradientSpreadMethod) -> Self {
391416
let stops = GradientStops::new([
392417
GradientStop {
393418
position: 0.,
@@ -401,7 +426,13 @@ impl Gradient {
401426
},
402427
]);
403428

404-
Self { start, end, stops, gradient_type }
429+
Self {
430+
start,
431+
end,
432+
stops,
433+
gradient_type,
434+
spread_method,
435+
}
405436
}
406437

407438
pub fn lerp(&self, other: &Self, time: f64) -> Self {
@@ -414,8 +445,15 @@ impl Gradient {
414445
});
415446
let stops = GradientStops::new(stops);
416447
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
448+
let spread_method = if time < 0.5 { self.spread_method } else { other.spread_method };
417449

418-
Self { start, end, stops, gradient_type }
450+
Self {
451+
start,
452+
end,
453+
stops,
454+
gradient_type,
455+
spread_method,
456+
}
419457
}
420458

421459
/// Insert a stop into the gradient, the index if successful

0 commit comments

Comments
 (0)