Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2633,7 +2633,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let mut row = vec![TextLabel::new("").widget_instance()];
add_blank_assist(&mut row);

let entries = [GradientType::Linear, GradientType::Radial]
let entries = [GradientType::Linear, GradientType::Radial, GradientType::Mesh]
.iter()
.map(|&grad_type| {
RadioEntryData::new(format!("{:?}", grad_type))
Expand Down
8 changes: 7 additions & 1 deletion editor/src/messages/tool/tool_messages/gradient_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,14 @@ impl LayoutHolder for GradientTool {
}
.into()
}),
RadioEntryData::new("Mesh").label("Mesh").tooltip_label(" Gradient").on_update(move |_| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::Type(GradientType::Mesh),
}
.into()
}),
])
.selected_index(Some((self.options.gradient_type == GradientType::Radial) as u32))
.selected_index(Some(self.options.gradient_type as u32))
.widget_instance();

// Display priority: the selected layer's stops, then any user-customized tool default, then the working colors
Expand Down
2 changes: 1 addition & 1 deletion node-graph/libraries/rendering/src/render_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ impl RenderExt for List<GradientStops> {
let gradient_id = generate_uuid();

match gradient_type {
GradientType::Linear => {
GradientType::Linear | GradientType::Mesh => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method}{gradient_transform}>{}</linearGradient>"#,
Expand Down
195 changes: 182 additions & 13 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
use crate::to_peniko::{BlendModeExt, ToPenikoColor};
use core_types::CacheHash;
use core_types::blending::BlendMode;
use core_types::bounds::BoundingBox;
use core_types::bounds::RenderBoundingBox;
use core_types::color::Color;
use core_types::color::SRGBA8;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::color::{Color, SRGBA8};
use core_types::consts::DEFAULT_FONT_SIZE;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List};
use core_types::math::quad::Quad;
Expand All @@ -18,7 +16,7 @@
ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
};
use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
use glam::{DAffine2, DMat2, DVec2, FloatExt};
use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource;
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
Expand All @@ -28,18 +26,19 @@
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
use graphic_types::{Artboard, Graphic, Vector};
use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts};
use num_traits::Zero;
use kurbo::{Affine, BezPath, Cap, Join, ParamCurve, Shape, StrokeOpts};
use num_traits::{Float, Zero};

Check failure on line 30 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused import: `Float`

Check warning on line 30 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused import: `Float`
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::{DrawSettings, OutlinePen};
use skrifa::raw::FontRef as SkrifaFontRef;
use skrifa::{GlyphId, MetadataProvider};
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::hash::Hash;
use std::ops::Deref;
use std::ops::{Add, Deref, Mul, Sub};

Check failure on line 38 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused imports: `Add`, `Mul`, and `Sub`

Check warning on line 38 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused imports: `Add`, `Mul`, and `Sub`
use std::sync::{Arc, LazyLock};
use vector_types::gradient::GradientSpreadMethod;
use vector_types::vector::misc::point_to_dvec2;
use vello::*;

#[derive(Clone, Copy, Debug, PartialEq)]
Expand Down Expand Up @@ -384,7 +383,7 @@
pub(crate) fn gradient_placement(transform: DAffine2, gradient_type: GradientType) -> DAffine2 {
match gradient_type {
GradientType::Radial => transform,
GradientType::Linear => {
GradientType::Linear | GradientType::Mesh => {
let axis = transform.matrix2.x_axis;
let band_normal = transform.matrix2.y_axis.perp();
let line = if band_normal.length_squared() > 0. { axis.project_onto(band_normal) } else { axis };
Expand Down Expand Up @@ -416,7 +415,7 @@

let brush = peniko::Brush::Gradient(peniko::Gradient {
kind: match gradient_type {
GradientType::Linear => peniko::LinearGradientPosition {
GradientType::Linear | GradientType::Mesh => peniko::LinearGradientPosition {
start: to_point(start),
end: to_point(end),
}
Expand Down Expand Up @@ -1091,7 +1090,7 @@
MaskType::Mask
};

let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL);
let fill_graphic_list: Option<std::borrow::Cow<'_, List<Graphic>>> = graphic_list_at(self, index, ATTR_FILL);
let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0));

let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE);
Expand Down Expand Up @@ -1154,6 +1153,176 @@
}
}

let is_mesh_fill = fill_graphic_list
.as_ref()
.is_some_and(|list| matches!(list.element(0), Some(Graphic::Gradient(gradient)) if gradient.attribute_cloned_or_default::<GradientType>(ATTR_GRADIENT_TYPE, 0) == GradientType::Mesh));
if is_mesh_fill {
let mesh_transform = element_transform * applied_stroke_transform;

// Split the mesh into 8 x 8 subgrids
let grid_num = 2;
let grid_stride_uv = 1. / grid_num as f64;

let segments: Vec<_> = vector.segment_iter().map(|(_, path_seg, _, _)| path_seg).collect();
let points = vector.point_domain.positions();

let top_seg = segments[0];
let right_seg = segments[1];
let bottom_seg = segments[2];
let left_seg = segments[3];

let top_left_color = Color::RED;
let top_right_color = Color::BLUE;
let bottom_left_color = Color::GREEN;
let bottom_right_color = Color::YELLOW;

let bilerp_points = |u: f64, v: f64| points[3] * (1. - u) * (1. - v) + points[2] * u * (1. - v) + points[0] * (1. - u) * v + points[1] * u * v;
let float_srgba_to_hex = |color: [f32; 4]| -> String {
let float_to_u8 = |x: f32| (x.clamp(0., 1.) * 255.).round() as u8;
SRGBA8 {
red: float_to_u8(color[0]),
green: float_to_u8(color[1]),
blue: float_to_u8(color[2]),
alpha: float_to_u8(color[3]),
}
.to_rgba_hex()
};

type Row = Vec<(DVec2, [f32; 4])>;
type Grid = Vec<Row>;

// Create the position and color info of the grid that is splitted from the original mesh
let mut grid_info: Grid = vec![];

for i in 0..=grid_num {
let u = i as f64 * grid_stride_uv;
let mut grid_info_row: Row = vec![];

for j in 0..=grid_num {
let v = j as f64 * grid_stride_uv;
let c1_u = point_to_dvec2(bottom_seg.eval(1. - u));
let c2_u = point_to_dvec2(top_seg.eval(u));
let d1_v = point_to_dvec2(left_seg.eval(v));
let d2_v = point_to_dvec2(right_seg.eval(1. - v));
let lc = (1. - v) * c1_u + v * c2_u;
let ld = (1. - u) * d1_v + u * d2_v;
let pos = mesh_transform.transform_point2(lc + ld - bilerp_points(u, v));

// We need to calculate the color for each grid points by sRGB since SVG uses sRGB for color interpolation.
// `color-interpolation="linearRGB"` is part of the SVG2 spec but not yet implemented in major browsers as of Jul. 2026.
// See also: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/color-interpolation
let bottom_left_gamma = bottom_left_color.to_gamma_srgb_channels();
let bottom_right_gamma = bottom_right_color.to_gamma_srgb_channels();
let top_left_gamma = top_left_color.to_gamma_srgb_channels();
let top_right_gamma = top_right_color.to_gamma_srgb_channels();
let color_gamma: [f32; 4] = std::array::from_fn(|i| {
bottom_left_gamma[i]
.lerp(bottom_right_gamma[i], u as f32)
.lerp(top_left_gamma[i].lerp(top_right_gamma[i], u as f32), v as f32)
});

grid_info_row.push((pos, color_gamma));
}
grid_info.push(grid_info_row);
}

// Create poloygons using the vertex position data
let mut idx = generate_uuid();

for i in 0..grid_num {
for j in 0..grid_num {
let (bl, bl_color) = grid_info[i][j];
let (tl, tl_color) = grid_info[i][j + 1];
let (br, br_color) = grid_info[i + 1][j];
let (tr, tr_color) = grid_info[i + 1][j + 1];
let DVec2 { x: bl_x, y: bl_y } = bl;

Check failure on line 1238 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `bl_y`

Check failure on line 1238 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `bl_x`

Check warning on line 1238 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused variable: `bl_y`

Check warning on line 1238 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused variable: `bl_x`
let DVec2 { x: tl_x, y: tl_y } = tl;

Check failure on line 1239 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `tl_y`

Check failure on line 1239 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `tl_x`

Check warning on line 1239 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused variable: `tl_y`

Check warning on line 1239 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused variable: `tl_x`
let DVec2 { x: br_x, y: br_y } = br;

Check failure on line 1240 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `br_y`

Check failure on line 1240 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `br_x`

Check warning on line 1240 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused variable: `br_y`

Check warning on line 1240 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused variable: `br_x`
let DVec2 { x: tr_x, y: tr_y } = tr;

Check failure on line 1241 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `tr_y`

Check failure on line 1241 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `tr_x`

Check warning on line 1241 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused variable: `tr_y`

Check warning on line 1241 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused variable: `tr_x`
let patch_transform = format_transform_matrix(DAffine2::from_cols(tr - tl, bl - tl, tl));

// linear gradient for the bottom line
write!(
&mut render.svg_defs,
r##"<linearGradient id="gb{idx}" x1="0" y1="1" x2="1" y2="1" gradientTransform="{patch_transform}" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#{}"/><stop offset="1" stop-color="#{}"/></linearGradient>"##,
float_srgba_to_hex(bl_color),
float_srgba_to_hex(br_color),
)
.unwrap();

// linear gradient for the top line
write!(
&mut render.svg_defs,
r##"<linearGradient id="gt{idx}" x1="0" y1="0" x2="1" y2="0" gradientTransform="{patch_transform}" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#{}"/><stop offset="1" stop-color="#{}"/></linearGradient>"##,
float_srgba_to_hex(tl_color),
float_srgba_to_hex(tr_color),
)
.unwrap();

// top mask gradient
write!(
&mut render.svg_defs,
r##"<linearGradient id="gm{idx}" x1="0.5" y1="0" x2="0.5" y2="1" gradientTransform="{patch_transform}" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#000"/></linearGradient>"##,
)
.unwrap();

// inflate a patch to hide gaps caused by anti-aliasing
let bottom_normal = (br - bl).perp().normalize();
let top_normal = (tl - tr).perp().normalize();
let left_normal = (bl - tl).perp().normalize();
let right_normal = (tr - br).perp().normalize();
let pad = 1.;
let bl = bl + (bottom_normal + left_normal) * pad;
let tl = tl + (top_normal + left_normal) * pad;
let br = br + (bottom_normal + right_normal) * pad;
let tr = tr + (top_normal + right_normal) * pad;
let DVec2 { x: bl_x, y: bl_y } = bl;
let DVec2 { x: tl_x, y: tl_y } = tl;
let DVec2 { x: br_x, y: br_y } = br;
let DVec2 { x: tr_x, y: tr_y } = tr;

// mask
let min_x = bl_x.min(tl_x.min(br_x.min(tr_x)));
let max_x = bl_x.max(tl_x.max(br_x.max(tr_x)));
let min_y = bl_y.min(tl_y.min(br_y.min(tr_y)));
let max_y = bl_y.max(tl_y.max(br_y.max(tr_y)));
let mask_width = max_x - min_x;
let mask_height = max_y - min_y;
write!(
&mut render.svg_defs,
r##"<mask id="m{idx}" x="{min_x}" y="{min_y}" width="{mask_width}" height="{mask_height}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
<rect
x="{min_x}"
y="{min_y}"
width="{mask_width}"
height="{mask_height}"
fill="url(#gm{idx})" /></mask>"##,
)
.unwrap();

render.leaf_tag("polygon", |attributes| {
attributes.push("points", format!("{bl_x},{bl_y} {br_x},{br_y} {tr_x},{tr_y} {tl_x},{tl_y}"));
attributes.push("fill", format!("url(#gb{idx})"));
});

render.leaf_tag("polygon", |attributes| {
attributes.push("points", format!("{bl_x},{bl_y} {br_x},{br_y} {tr_x},{tr_y} {tl_x},{tl_y}"));
attributes.push("fill", format!("url(#gt{idx})"));
attributes.push("mask", format!("url(#m{idx})"));
});

idx += 1;
}
}

// Create a linear gradients, bottom-left -> bottom-right

// Create a linear gradient mask from top to bottom

// Create a linear gradient, top->left -> top->right with applying the mask
return;
}

render.leaf_tag("path", |attributes| {
attributes.push("d", path.clone());
let matrix = format_transform_matrix(element_transform);
Expand Down Expand Up @@ -2114,7 +2283,7 @@

// The unit gradient line is the +X unit vector in local space, before the item's transform is applied
match gradient_type {
GradientType::Linear => {
GradientType::Linear | GradientType::Mesh => {
let _ = write!(
&mut attributes.0.svg_defs,
r#"<linearGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</linearGradient>"#
Expand Down Expand Up @@ -2181,7 +2350,7 @@
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied.
// For radial, the unit-radius circle at the origin scales out to the line's length once the brush transform applies.
let kind = match gradient_type {
GradientType::Linear => peniko::LinearGradientPosition {
GradientType::Linear | GradientType::Mesh => peniko::LinearGradientPosition {
start: to_point(DVec2::ZERO),
end: to_point(DVec2::X),
}
Expand Down
1 change: 1 addition & 0 deletions node-graph/libraries/vector-types/src/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub enum GradientType {
#[default]
Linear,
Radial,
Mesh,
}

// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
Expand Down
Loading