Add support for setting the spread method for gradient fills (#3953)

* 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

* Sync gradient tool options with layer's gradient

* Sync gradient_type and spread_method from the selected
  layer's existing gradient to the tool options bar when
  switching to the gradient tool

* Refactor has_gradient_on_selected_layers
  to reuse a new get_gradient_on_selected_layer helper

* Swap Reflect and Repeat order in UI radio buttons

* Fix alignment of the radio buttons in right panel

* Fix the position of the radio buttons in the tool

* Rename SpreadMethod to SetSpreadMethod

* Move default spread method omission logic
This commit is contained in:
YohYamasaki
2026-04-14 10:09:57 +02:00
committed by GitHub
parent da45ab2f87
commit 79d778a535
6 changed files with 232 additions and 14 deletions

View File

@@ -4,6 +4,7 @@ use glam::DAffine2;
use graphic_types::vector_types::gradient::{Gradient, GradientType};
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write;
use vector_types::gradient::GradientSpreadMethod;
pub trait RenderExt {
type Output;
@@ -47,13 +48,19 @@ impl RenderExt for Gradient {
format!(r#" gradientTransform="{gradient_transform}""#)
};
let spread_method = if self.spread_method == GradientSpreadMethod::Pad {
String::new()
} else {
format!(r#" spreadMethod="{}""#, self.spread_method.svg_name())
};
let gradient_id = generate_uuid();
match self.gradient_type {
GradientType::Linear => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{gradient_transform}>{}</linearGradient>"#,
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{spread_method}{gradient_transform}>{}</linearGradient>"#,
gradient_id, start.x, start.y, end.x, end.y, stop
);
}
@@ -61,7 +68,7 @@ impl RenderExt for Gradient {
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
let _ = write!(
svg_defs,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{gradient_transform}>{}</radialGradient>"#,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{spread_method}{gradient_transform}>{}</radialGradient>"#,
gradient_id, start.x, start.y, radius, stop
);
}

View File

@@ -24,6 +24,7 @@ use std::fmt::Write;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::sync::{Arc, LazyLock};
use vector_types::gradient::GradientSpreadMethod;
use vello::*;
/// Cached 16x16 transparency checkerboard image data (two 8x8 cells of #ffffff and #cccccc).
@@ -1159,6 +1160,11 @@ impl Render for Table<Vector> {
.into()
}
},
extend: match gradient.spread_method {
GradientSpreadMethod::Pad => peniko::Extend::Pad,
GradientSpreadMethod::Reflect => peniko::Extend::Reflect,
GradientSpreadMethod::Repeat => peniko::Extend::Repeat,
},
stops,
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
..Default::default()

View File

@@ -334,6 +334,27 @@ impl GradientStops {
}
}
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum GradientSpreadMethod {
#[default]
Pad,
Reflect,
Repeat,
}
impl GradientSpreadMethod {
pub fn svg_name(&self) -> &'static str {
match self {
GradientSpreadMethod::Pad => "pad",
GradientSpreadMethod::Reflect => "reflect",
GradientSpreadMethod::Repeat => "repeat",
}
}
}
/// A gradient fill.
///
/// Contains the start and end points, along with the colors at varying points along the length.
@@ -345,6 +366,8 @@ pub struct Gradient {
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
#[serde(default)]
pub spread_method: GradientSpreadMethod,
}
impl Default for Gradient {
@@ -354,6 +377,7 @@ impl Default for Gradient {
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
spread_method: GradientSpreadMethod::Pad,
}
}
}
@@ -369,6 +393,7 @@ impl std::hash::Hash for Gradient {
.for_each(|x| x.to_bits().hash(state));
self.stops.color.iter().for_each(|color| color.hash(state));
self.gradient_type.hash(state);
self.spread_method.hash(state);
}
}
@@ -387,7 +412,7 @@ impl std::fmt::Display for Gradient {
impl Gradient {
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType) -> Self {
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType, spread_method: GradientSpreadMethod) -> Self {
let stops = GradientStops::new([
GradientStop {
position: 0.,
@@ -401,7 +426,13 @@ impl Gradient {
},
]);
Self { start, end, stops, gradient_type }
Self {
start,
end,
stops,
gradient_type,
spread_method,
}
}
pub fn lerp(&self, other: &Self, time: f64) -> Self {
@@ -414,8 +445,15 @@ impl Gradient {
});
let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
let spread_method = if time < 0.5 { self.spread_method } else { other.spread_method };
Self { start, end, stops, gradient_type }
Self {
start,
end,
stops,
gradient_type,
spread_method,
}
}
/// Insert a stop into the gradient, the index if successful