Skip to content

Add Gradient in Stroke Node#3905

Open
mohan-bee wants to merge 2 commits intoGraphiteEditor:masterfrom
mohan-bee:feat/stroke-gradient
Open

Add Gradient in Stroke Node#3905
mohan-bee wants to merge 2 commits intoGraphiteEditor:masterfrom
mohan-bee:feat/stroke-gradient

Conversation

@mohan-bee
Copy link
Contributor

closes #3816

I added gradient support to strokes, unifying strokes and fills under the same paint model, and updated the ui, svg export, and GPU renderer to support it end-to-end.

demo:

Screen_Recording_2026-03-16_at_12.34.47_PM.mov

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces comprehensive gradient support for strokes, aligning their painting capabilities with existing fill functionalities. The changes span across the editor's UI, SVG export logic, and the GPU renderer, ensuring a consistent and fully functional experience for applying gradients to vector strokes. This enhancement provides greater creative flexibility by allowing users to define complex visual styles for their vector paths.

Highlights

  • Gradient Support for Strokes: Implemented the ability to apply gradients to strokes, moving beyond solid color strokes.
  • Unified Paint Model: Unified the paint model for strokes and fills, allowing strokes to use the same Fill enum (None, Solid, Gradient) as fills.
  • UI Updates for Stroke Properties: Updated the UI for stroke properties to include controls for selecting between solid colors and various gradient types (linear, radial), along with options to reverse gradients.
  • SVG Export Compatibility: Ensured that stroke gradients are correctly exported to SVG format.
  • GPU Renderer Integration: Integrated gradient stroke rendering into the GPU renderer for accurate visual representation.
Changelog
  • editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs
    • Updated stroke color retrieval to use the new stroke.color() method, reflecting the change from Option<Color> to Fill.
  • editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs
    • Modified the apply_usvg_stroke function to assign Fill::Solid to the stroke's paint property when importing USVG nodes, replacing the direct Option<Color> assignment.
  • editor/src/messages/portfolio/document/graph_operation/utility_types.rs
    • Refactored ModifyInputsContext::stroke_set to handle Fill enum variants (None, Solid, Gradient) for stroke paint, introducing separate input connectors for backup color and gradient values.
  • editor/src/messages/portfolio/document/node_graph/node_properties.rs
    • Imported the Gradient type for use in stroke properties.
    • Completely overhauled the stroke_properties function to display a new UI for stroke color, allowing selection between solid and gradient fills, and providing controls for gradient manipulation (e.g., reverse, type selection).
    • Introduced logic to store backup color and gradient values when switching between fill types in the UI.
  • node-graph/libraries/rendering/src/render_ext.rs
    • Adjusted the Stroke::render method to generate SVG attributes based on the Fill enum, supporting solid colors and generating url(#gradient_id) for gradients.
    • Removed the early return for invisible strokes based on Option<Color> and integrated has_renderable_stroke with the new Fill logic.
  • node-graph/libraries/rendering/src/renderer.rs
    • Updated the Render for Table<Vector> implementation to process stroke Fill types, converting solid colors to peniko::Brush::Solid and gradients to peniko::Brush::Gradient with appropriate transformations.
    • Changed the kurbo::Stroke creation to use the new brush and brush_transform for rendering strokes on the GPU.
  • node-graph/libraries/vector-types/src/vector/style.rs
    • Modified Fill::color() to return Color::TRANSPARENT for Fill::None and empty Fill::Gradient.
    • Added From<f64> and From<Vec<f64>> implementations for the Fill enum.
    • Refactored the Stroke struct to replace pub color: Option<Color> with pub paint: Fill.
    • Updated Stroke::hash, Stroke::new, Stroke::lerp, Stroke::color(), Stroke::with_color, Stroke::has_renderable_stroke, and Stroke::default to correctly handle the new paint: Fill field.
    • Adjusted the PathStyle display implementation to format stroke information using the Fill enum.
  • node-graph/nodes/vector/src/vector_nodes.rs
    • Added #[default] attributes for _backup_color and _backup_gradient inputs in the fill node.
    • Modified the stroke node to accept a generic F: Into<Fill> for its color input, expanding its #[implementations] to support various Fill related types.
    • Introduced _backup_color and _backup_gradient inputs to the stroke node.
    • Updated the internal Stroke creation within the stroke node to use paint: color.into().
    • Changed the solidify_stroke function to directly use stroke.paint when setting the fill style.
Activity
  • The pull request addresses and closes issue Gradients within strokes #3816.
  • A demo video showcasing the new gradient stroke functionality has been provided by the author.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@mohan-bee mohan-bee force-pushed the feat/stroke-gradient branch from 4c876fd to dd27358 Compare March 16, 2026 08:59
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request successfully integrates gradient support for strokes, unifying the paint model across strokes and fills. The changes are well-implemented across the UI, SVG export, and GPU renderer, demonstrating a comprehensive approach to the feature. The refactoring of the Stroke struct to use the Fill enum is a key improvement, and the corresponding updates in various modules are consistent and correct. The addition of backup inputs for color and gradient in the node properties ensures a smooth user experience when switching between fill types. Overall, the code is clean, follows good practices, and effectively delivers the intended functionality.

@mohan-bee
Copy link
Contributor Author

@Keavon review pls

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs:392">
P2: Stroke paint is reduced to solid/none in the data panel, so gradient stroke paint is displayed inaccurately.</violation>
</file>

<file name="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs:499">
P1: SVG import stroke handling ignores gradient paints, causing gradient-stroked paths to lose intended stroke styling on import.</violation>
</file>

<file name="node-graph/libraries/vector-types/src/vector/style.rs">

<violation number="1" location="node-graph/libraries/vector-types/src/vector/style.rs:155">
P2: `From<f64>`/`From<Vec<f64>>` for `Fill` silently drops values by always returning `Fill::None`, masking type/wiring errors through implicit `Into<Fill>` coercion.</violation>

<violation number="2" location="node-graph/libraries/vector-types/src/vector/style.rs:316">
P1: Renaming serialized `Stroke.color` to `paint` without deserialization alias/migration drops legacy stroke colors when loading older documents.</violation>

<violation number="3" location="node-graph/libraries/vector-types/src/vector/style.rs:501">
P2: `Stroke::default()` now initializes with `Fill::None`, contradicting the documented UX requirement to start with an opaque default stroke paint.</violation>
</file>

<file name="editor/src/messages/portfolio/document/node_graph/node_properties.rs">

<violation number="1" location="editor/src/messages/portfolio/document/node_graph/node_properties.rs:2056">
P2: Stroke color inline controls are rendered even when the input is exposed to the graph, creating conflicting UI and updates.</violation>

<violation number="2" location="editor/src/messages/portfolio/document/node_graph/node_properties.rs:2061">
P2: Backup stroke fill state is written from stale pre-edit `fill2` instead of the newly selected picker value, causing type-toggle restore to lose the latest edit.</violation>
</file>

<file name="node-graph/nodes/vector/src/vector_nodes.rs">

<violation number="1" location="node-graph/nodes/vector/src/vector_nodes.rs:203">
P2: Stroke node now defaults to `Fill::None`, causing newly added strokes to be invisible by default (regression from visible black stroke).</violation>

<violation number="2" location="node-graph/nodes/vector/src/vector_nodes.rs:1242">
P2: `solidify_stroke` copies gradient paint without remapping to the original stroke bounds, which can visually shift/stretch gradients after conversion.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if let usvg::Paint::Color(color) = &stroke.paint() {
modify_inputs.stroke_set(Stroke {
color: Some(usvg_color(*color, stroke.opacity().get())),
paint: Fill::Solid(usvg_color(*color, stroke.opacity().get())),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: SVG import stroke handling ignores gradient paints, causing gradient-stroked paths to lose intended stroke styling on import.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, line 499:

<comment>SVG import stroke handling ignores gradient paints, causing gradient-stroked paths to lose intended stroke styling on import.</comment>

<file context>
@@ -496,7 +496,7 @@ fn import_usvg_node(
 	if let usvg::Paint::Color(color) = &stroke.paint() {
 		modify_inputs.stroke_set(Stroke {
-			color: Some(usvg_color(*color, stroke.opacity().get())),
+			paint: Fill::Solid(usvg_color(*color, stroke.opacity().get())),
 			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(),
</file context>

/// Stroke color
pub color: Option<Color>,
/// Stroke paint (solid color or gradient)
pub paint: Fill,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Renaming serialized Stroke.color to paint without deserialization alias/migration drops legacy stroke colors when loading older documents.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/vector-types/src/vector/style.rs, line 316:

<comment>Renaming serialized `Stroke.color` to `paint` without deserialization alias/migration drops legacy stroke colors when loading older documents.</comment>

<file context>
@@ -300,8 +312,8 @@ fn daffine2_identity() -> DAffine2 {
-	/// Stroke color
-	pub color: Option<Color>,
+	/// Stroke paint (solid color or gradient)
+	pub paint: Fill,
 	/// Line thickness
 	pub weight: f64,
</file context>

}
}

impl From<f64> for Fill {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: From<f64>/From<Vec<f64>> for Fill silently drops values by always returning Fill::None, masking type/wiring errors through implicit Into<Fill> coercion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/vector-types/src/vector/style.rs, line 155:

<comment>`From<f64>`/`From<Vec<f64>>` for `Fill` silently drops values by always returning `Fill::None`, masking type/wiring errors through implicit `Into<Fill>` coercion.</comment>

<file context>
@@ -152,6 +152,18 @@ impl From<Gradient> for Fill {
 	}
 }
 
+impl From<f64> for Fill {
+	fn from(_: f64) -> Fill {
+		Fill::None
</file context>

@mohan-bee mohan-bee force-pushed the feat/stroke-gradient branch from dd27358 to 871468d Compare March 16, 2026 13:51
let color = color_widget(
ParameterWidgetsInfo::new(node_id, ColorInput::INDEX, true, context),
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
let fill = document_node
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This giant block of code starting here should not be duplicated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right @Keavon , thanks for catching that! I'll refactor this by extracting the shared fill/gradient widget-building logic into a common helper function (something like fill_color_widgets()) that both fill_properties and stroke_properties can call, so the code isn't duplicated. I hope this resolves.

@Keavon Keavon force-pushed the master branch 2 times, most recently from d6228da to e58c1de Compare March 16, 2026 23:03
@GraphiteEditor GraphiteEditor deleted a comment from github-actions bot Mar 16, 2026
@Keavon
Copy link
Member

Keavon commented Mar 16, 2026

!build (Run ID 23170598336)

@github-actions
Copy link

📦 Web Build Complete for 871468d
https://28492283.graphite.pages.dev

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="node-graph/libraries/rendering/src/renderer.rs">

<violation number="1" location="node-graph/libraries/rendering/src/renderer.rs:1712">
P2: Standalone Fill::Gradient rendering to Vello collapses gradients to a single stop color, so GPU output renders a flat color instead of the intended gradient.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

let color = match &row.element {
vector_types::vector::style::Fill::None => continue,
vector_types::vector::style::Fill::Solid(color) => *color,
vector_types::vector::style::Fill::Gradient(gradient) => gradient.stops.color.first().copied().unwrap_or(Color::MAGENTA),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Standalone Fill::Gradient rendering to Vello collapses gradients to a single stop color, so GPU output renders a flat color instead of the intended gradient.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/rendering/src/renderer.rs, line 1712:

<comment>Standalone Fill::Gradient rendering to Vello collapses gradients to a single stop color, so GPU output renders a flat color instead of the intended gradient.</comment>

<file context>
@@ -1607,11 +1607,153 @@ impl Render for Table<Color> {
+			let color = match &row.element {
+				vector_types::vector::style::Fill::None => continue,
+				vector_types::vector::style::Fill::Solid(color) => *color,
+				vector_types::vector::style::Fill::Gradient(gradient) => gradient.stops.color.first().copied().unwrap_or(Color::MAGENTA),
+			};
+
</file context>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a known limitation that the renderer currently falls back to the first stop color for standalone gradient layers. A proper fix requires a different approach. Leaving this as a known stub for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gradients within strokes

2 participants