From c82da4dbd7da4503700be7aaf18f755dae7d21e5 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Sun, 19 Apr 2026 22:37:52 +0900 Subject: [PATCH 1/8] wip: add mesh enum --- .../document/node_graph/node_properties.rs | 2 +- .../tool/tool_messages/gradient_tool.rs | 8 +++- .../libraries/rendering/src/render_ext.rs | 2 +- .../libraries/rendering/src/renderer.rs | 16 +++++-- .../libraries/vector-types/src/gradient.rs | 1 + node-graph/nodes/vector/src/vector_nodes.rs | 45 +++++++++++++++++++ 6 files changed, 67 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index b61c33de82..ff9fa6c55c 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2674,7 +2674,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)) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 940d7e6552..7ab9edb15c 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -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 diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index 1113e5816c..9d2ef67b0c 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -138,7 +138,7 @@ impl RenderExt for List { let gradient_id = generate_uuid(); match gradient_type { - GradientType::Linear => { + GradientType::Linear | GradientType::Mesh => { let _ = write!( svg_defs, r#"{}"#, diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 4367dd53c0..24254e43f6 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -382,7 +382,7 @@ pub(crate) fn transform_is_invertible(transform: DAffine2) -> bool { 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 }; @@ -414,7 +414,7 @@ fn create_peniko_gradient_brush(gradient_list: &List, multiplied_trans 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), } @@ -1160,6 +1160,14 @@ impl Render for List { } } + 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::(ATTR_GRADIENT_TYPE, 0) == GradientType::Mesh) + }); + if is_mesh_fill { + log::debug!("hoge"); + return; + } + render.leaf_tag("path", |attributes| { attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); @@ -2122,7 +2130,7 @@ impl Render for List { // 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#"{stop_string}"# @@ -2189,7 +2197,7 @@ impl Render for List { // 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), } diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 0a9807d0aa..1de108c660 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -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 diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index c2efc76a16..658efd26fe 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -173,6 +173,51 @@ where content } +// #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] +// async fn mesh_gradient( +// _: impl Ctx, +// /// The content with vector paths to apply the mesh gradient to. +// mut content: Table, +// /// The top-left color of the gradient. +// #[default(Color::BLACK)] +// top_left_color: Table, +// /// The top-right color of the gradient. +// #[default(Color::BLACK)] +// top_right_color: Table, +// /// The bottom-left color of the gradient. +// #[default(Color::BLACK)] +// bottom_left_color: Table, +// /// The bottom-right color of the gradient. +// #[default(Color::BLACK)] +// bottom_right_color: Table, +// _backup_color: Table, +// _backup_gradient: Gradient, +// ) -> Table { +// for vector in content.vector_iter_mut() { +// let grid_num = 8; +// let grid_stride_uv = 1. / grid_num as f64; + +// let segments: Vec<_> = vector.element.segment_iter().collect(); +// let points = vector.element.point_domain.positions(); +// let top_seg = segments.get(0).unwrap_throw().1; +// let right_seg = segments.get(1).unwrap_throw().1; +// let bottom_seg = segments.get(2).unwrap_throw().1; +// let left_seg = segments.get(3).unwrap_throw().1; + +// let bilerp = |u: f64, v: f64| points[0] * (1. - u) * (1. - v) + points[1] * u * (1. - v) + points[2] * (1. - u) * v + points[3] * u * v; + +// for i in 0..=grid_num { +// for j in 0..=grid_num { +// let u = i as f64 * grid_stride_uv; +// let v = j as f64 * grid_stride_uv; +// let pos = point_to_dvec2(top_seg.eval(u)) + point_to_dvec2(bottom_seg.eval(1. - u)) - bilerp(u, v); +// } +// } +// } + +// content +// } + /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] async fn fill( From 00a7709dd2de49d5d85559bbbf01389d907df6ad Mon Sep 17 00:00:00 2001 From: Yohei <74522538+YohyYamasaki@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:11:57 +0900 Subject: [PATCH 2/8] wip: first poc Approximate Coons patch by creating bilinear gradient in parallelograms. Each subgrid's color does not have C1 continuity, so we can see steps of colors in the strongly skewed scene. This could be solved by using bicubic interpolation. --- .../no-std-types/src/color/color_types.rs | 14 ++ .../libraries/rendering/src/renderer.rs | 127 +++++++++++++++++- node-graph/nodes/vector/src/vector_nodes.rs | 45 ------- 3 files changed, 135 insertions(+), 51 deletions(-) diff --git a/node-graph/libraries/no-std-types/src/color/color_types.rs b/node-graph/libraries/no-std-types/src/color/color_types.rs index 56ba5d958e..76b2d4ba9b 100644 --- a/node-graph/libraries/no-std-types/src/color/color_types.rs +++ b/node-graph/libraries/no-std-types/src/color/color_types.rs @@ -1057,6 +1057,20 @@ impl Color { } } +pub fn bilerp_color(c00: Color, c10: Color, c01: Color, c11: Color, u: f64, v: f64) -> Color { + let (u, v) = (u as f32, v as f32); + let w00 = (1. - u) * (1. - v); + let w10 = u * (1. - v); + let w01 = (1. - u) * v; + let w11 = u * v; + Color::from_rgbaf32_unchecked( + w00 * c00.r() + w10 * c10.r() + w01 * c01.r() + w11 * c11.r(), + w00 * c00.g() + w10 * c10.g() + w01 * c01.g() + w11 * c11.g(), + w00 * c00.b() + w10 * c10.b() + w01 * c01.b() + w11 * c11.b(), + w00 * c00.a() + w10 * c10.a() + w01 * c01.a() + w11 * c11.a(), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 24254e43f6..0b00cac56a 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2,10 +2,8 @@ use crate::render_ext::{PaintTarget, RenderExt}; 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, bilerp_color}; use core_types::consts::DEFAULT_FONT_SIZE; use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List, NodeIdPath}; use core_types::math::quad::Quad; @@ -28,7 +26,7 @@ use graphic_types::vector_types::subpath::Subpath; 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 kurbo::{Affine, BezPath, Cap, Join, ParamCurve, Shape, StrokeOpts}; use num_traits::Zero; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; @@ -40,6 +38,7 @@ use std::hash::Hash; use std::ops::Deref; 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)] @@ -1164,7 +1163,123 @@ impl Render for List { matches!(list.element(0), Some(Graphic::Gradient(gradient)) if gradient.attribute_cloned_or_default::(ATTR_GRADIENT_TYPE, 0) == GradientType::Mesh) }); if is_mesh_fill { - log::debug!("hoge"); + // Split the mesh into 8 x 8 subgrids + let grid_num = 8; + 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 = |u: f64, v: f64| points[3] * (1. - u) * (1. - v) + points[2] * u * (1. - v) + points[0] * (1. - u) * v + points[1] * u * v; + + // Create the position and color info of the grid that is splitted from the original mesh + let mut grid_info: Vec> = vec![]; + + for i in 0..=grid_num { + let u = i as f64 * grid_stride_uv; + let mut grid_info_row: Vec<(DVec2, Color)> = 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 = lc + ld - bilerp(u, v); + let color = bilerp_color(bottom_left_color, bottom_right_color, top_left_color, top_right_color, u, v); + grid_info_row.push((pos, color)); + } + 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 = grid_info[i][j]; + let tl = grid_info[i][j + 1]; + let br = grid_info[i + 1][j]; + let tr = grid_info[i + 1][j + 1]; + + // linear gradient for the bottom line + write!( + &mut render.svg_defs, + r##""##, + bl.0.x, + bl.0.y, + br.0.x, + br.0.y, + SRGBA8::from(bl.1).to_rgb_hex(), + SRGBA8::from(br.1).to_rgb_hex(), + ) + .unwrap(); + + // linear gradient for the top line + write!( + &mut render.svg_defs, + r##""##, + tl.0.x, + tl.0.y, + tr.0.x, + tr.0.y, + SRGBA8::from(tl.1).to_rgb_hex(), + SRGBA8::from(tr.1).to_rgb_hex(), + ) + .unwrap(); + + // top mask gradient + write!( + &mut render.svg_defs, + r##""##, + (bl.0.x + br.0.x) / 2., + (bl.0.y + br.0.y) / 2., + (tl.0.x + tr.0.x) / 2., + (tl.0.y + tr.0.y) / 2., + ) + .unwrap(); + + // mask + write!( + &mut render.svg_defs, + r##""##, + bl.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.y, + ) + .unwrap(); + + render.leaf_tag("polygon", |attributes| { + attributes.push("points", format!("{},{} {},{} {},{} {},{}", bl.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.y)); + attributes.push("fill", format!("url(#gb{idx})")); + }); + + render.leaf_tag("polygon", |attributes| { + attributes.push("points", format!("{},{} {},{} {},{} {},{}", bl.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.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; } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 658efd26fe..c2efc76a16 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -173,51 +173,6 @@ where content } -// #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -// async fn mesh_gradient( -// _: impl Ctx, -// /// The content with vector paths to apply the mesh gradient to. -// mut content: Table, -// /// The top-left color of the gradient. -// #[default(Color::BLACK)] -// top_left_color: Table, -// /// The top-right color of the gradient. -// #[default(Color::BLACK)] -// top_right_color: Table, -// /// The bottom-left color of the gradient. -// #[default(Color::BLACK)] -// bottom_left_color: Table, -// /// The bottom-right color of the gradient. -// #[default(Color::BLACK)] -// bottom_right_color: Table, -// _backup_color: Table, -// _backup_gradient: Gradient, -// ) -> Table { -// for vector in content.vector_iter_mut() { -// let grid_num = 8; -// let grid_stride_uv = 1. / grid_num as f64; - -// let segments: Vec<_> = vector.element.segment_iter().collect(); -// let points = vector.element.point_domain.positions(); -// let top_seg = segments.get(0).unwrap_throw().1; -// let right_seg = segments.get(1).unwrap_throw().1; -// let bottom_seg = segments.get(2).unwrap_throw().1; -// let left_seg = segments.get(3).unwrap_throw().1; - -// let bilerp = |u: f64, v: f64| points[0] * (1. - u) * (1. - v) + points[1] * u * (1. - v) + points[2] * (1. - u) * v + points[3] * u * v; - -// for i in 0..=grid_num { -// for j in 0..=grid_num { -// let u = i as f64 * grid_stride_uv; -// let v = j as f64 * grid_stride_uv; -// let pos = point_to_dvec2(top_seg.eval(u)) + point_to_dvec2(bottom_seg.eval(1. - u)) - bilerp(u, v); -// } -// } -// } - -// content -// } - /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] async fn fill( From 3d4d1f88e79fa4682cfa1281b99b8e2d05c872b2 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Fri, 10 Jul 2026 14:23:07 +0900 Subject: [PATCH 3/8] Fix gradient transform for patches --- .../no-std-types/src/color/color_types.rs | 14 -- .../libraries/rendering/src/renderer.rs | 130 ++++++++++++------ 2 files changed, 88 insertions(+), 56 deletions(-) diff --git a/node-graph/libraries/no-std-types/src/color/color_types.rs b/node-graph/libraries/no-std-types/src/color/color_types.rs index 76b2d4ba9b..56ba5d958e 100644 --- a/node-graph/libraries/no-std-types/src/color/color_types.rs +++ b/node-graph/libraries/no-std-types/src/color/color_types.rs @@ -1057,20 +1057,6 @@ impl Color { } } -pub fn bilerp_color(c00: Color, c10: Color, c01: Color, c11: Color, u: f64, v: f64) -> Color { - let (u, v) = (u as f32, v as f32); - let w00 = (1. - u) * (1. - v); - let w10 = u * (1. - v); - let w01 = (1. - u) * v; - let w11 = u * v; - Color::from_rgbaf32_unchecked( - w00 * c00.r() + w10 * c10.r() + w01 * c01.r() + w11 * c11.r(), - w00 * c00.g() + w10 * c10.g() + w01 * c01.g() + w11 * c11.g(), - w00 * c00.b() + w10 * c10.b() + w01 * c01.b() + w11 * c11.b(), - w00 * c00.a() + w10 * c10.a() + w01 * c01.a() + w11 * c11.a(), - ) -} - #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 0b00cac56a..594c8ff0aa 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -3,7 +3,7 @@ use crate::to_peniko::{BlendModeExt, ToPenikoColor}; use core_types::CacheHash; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; -use core_types::color::{Color, SRGBA8, bilerp_color}; +use core_types::color::{Color, SRGBA8}; use core_types::consts::DEFAULT_FONT_SIZE; use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List, NodeIdPath}; use core_types::math::quad::Quad; @@ -16,7 +16,7 @@ use core_types::{ 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}; @@ -27,7 +27,7 @@ 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, ParamCurve, Shape, StrokeOpts}; -use num_traits::Zero; +use num_traits::{Float, Zero}; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; use skrifa::raw::FontRef as SkrifaFontRef; @@ -35,7 +35,7 @@ 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}; use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; use vector_types::vector::misc::point_to_dvec2; @@ -1096,7 +1096,7 @@ impl Render for List { MaskType::Mask }; - let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL); + let fill_graphic_list: Option>> = 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); @@ -1159,12 +1159,14 @@ impl Render for List { } } - 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::(ATTR_GRADIENT_TYPE, 0) == GradientType::Mesh) - }); + 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::(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 = 8; + 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(); @@ -1180,14 +1182,27 @@ impl Render for List { let bottom_left_color = Color::GREEN; let bottom_right_color = Color::YELLOW; - let bilerp = |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 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; // Create the position and color info of the grid that is splitted from the original mesh - let mut grid_info: Vec> = vec![]; + 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: Vec<(DVec2, Color)> = vec![]; + let mut grid_info_row: Row = vec![]; for j in 0..=grid_num { let v = j as f64 * grid_stride_uv; @@ -1197,10 +1212,22 @@ impl Render for List { 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) + }); - let pos = lc + ld - bilerp(u, v); - let color = bilerp_color(bottom_left_color, bottom_right_color, top_left_color, top_right_color, u, v); - grid_info_row.push((pos, color)); + grid_info_row.push((pos, color_gamma)); } grid_info.push(grid_info_row); } @@ -1210,63 +1237,82 @@ impl Render for List { for i in 0..grid_num { for j in 0..grid_num { - let bl = grid_info[i][j]; - let tl = grid_info[i][j + 1]; - let br = grid_info[i + 1][j]; - let tr = grid_info[i + 1][j + 1]; + 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; + 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; + 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##""##, - bl.0.x, - bl.0.y, - br.0.x, - br.0.y, - SRGBA8::from(bl.1).to_rgb_hex(), - SRGBA8::from(br.1).to_rgb_hex(), + r##""##, + 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##""##, - tl.0.x, - tl.0.y, - tr.0.x, - tr.0.y, - SRGBA8::from(tl.1).to_rgb_hex(), - SRGBA8::from(tr.1).to_rgb_hex(), + r##""##, + float_srgba_to_hex(tl_color), + float_srgba_to_hex(tr_color), ) .unwrap(); // top mask gradient write!( &mut render.svg_defs, - r##""##, - (bl.0.x + br.0.x) / 2., - (bl.0.y + br.0.y) / 2., - (tl.0.x + tr.0.x) / 2., - (tl.0.y + tr.0.y) / 2., + r##""##, ) .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##""##, - bl.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.y, + r##" + "##, ) .unwrap(); render.leaf_tag("polygon", |attributes| { - attributes.push("points", format!("{},{} {},{} {},{} {},{}", bl.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.y)); + 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.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.y)); + 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})")); }); From dc41be8fb2f21469a255c21219e332b0fa2c824a Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Tue, 14 Jul 2026 12:15:41 +0900 Subject: [PATCH 4/8] Update PoC - Use bicubic hermite interpolation for color - Consider 2x2 patches (9 control points) --- .../libraries/rendering/src/renderer.rs | 400 ++++++++++++------ 1 file changed, 281 insertions(+), 119 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 594c8ff0aa..b1950da762 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -16,7 +16,7 @@ use core_types::{ ATTR_TEXT_ALIGN, ATTR_TRANSFORM, }; use dyn_any::DynAny; -use glam::{DAffine2, DMat2, DVec2, FloatExt}; +use glam::{DAffine2, DMat2, DVec2, FloatExt, Vec4}; use graphene_hash::CacheHashWrapper; use graphene_resource::Resource; use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute}; @@ -26,7 +26,7 @@ use graphic_types::vector_types::subpath::Subpath; 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, ParamCurve, Shape, StrokeOpts}; +use kurbo::{Affine, BezPath, Cap, CubicBez, Join, ParamCurve, PathSeg, Shape, StrokeOpts}; use num_traits::{Float, Zero}; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; @@ -1164,25 +1164,130 @@ impl Render for List { .is_some_and(|list| matches!(list.element(0), Some(Graphic::Gradient(gradient)) if gradient.attribute_cloned_or_default::(ATTR_GRADIENT_TYPE, 0) == GradientType::Mesh)); if is_mesh_fill { let mesh_transform = element_transform * applied_stroke_transform; + let mesh_corner_count_column: usize = 3; + let mesh_corner_count_row: usize = 3; + let patch_count_column = mesh_corner_count_column - 1; + let patch_count_row = mesh_corner_count_row - 1; + let subpatch_count_per_patch = 4; + let subpatch_count_per_axis = patch_count_row * subpatch_count_per_patch; + let subpatch_stride_uv = 1. / subpatch_count_per_axis as f64; + + let top_left_color = Color::BLACK; + let top_middle_color = Color::WHITE; + let top_right_color = Color::BLACK; + + let middle_left_color = Color::WHITE; + let center_color = Color::BLACK; + let middle_right_color = Color::WHITE; + + let bottom_left_color = Color::BLACK; + let bottom_middle_color = Color::WHITE; + let bottom_right_color = Color::BLACK; + + let mesh_corner_colors = vec![ + bottom_left_color, + bottom_middle_color, + bottom_right_color, + middle_left_color, + center_color, + middle_right_color, + top_left_color, + top_middle_color, + top_right_color, + ]; + + let mesh_corner_pos = vec![ + DVec2::new(0., 0.), + DVec2::new(550., 50.), + DVec2::new(1100., 0.), + DVec2::new(-50., 450.), + DVec2::new(575., 550.), + DVec2::new(1150., 425.), + DVec2::new(0., 950.), + DVec2::new(525., 900.), + DVec2::new(1100., 1000.), + ]; + + let horizontal_segments = [ + PathSeg::Cubic(CubicBez::new((0., 0.), (183.335, 16.665), (366.665, 50.), (550., 50.))), + PathSeg::Cubic(CubicBez::new((550., 50.), (733.335, 50.), (916.665, 16.665), (1100., 0.))), + PathSeg::Cubic(CubicBez::new((-50., 450.), (158.335, 483.335), (375., 554.165), (575., 550.))), + PathSeg::Cubic(CubicBez::new((575., 550.), (775., 545.835), (958.335, 466.665), (1150., 425.))), + PathSeg::Cubic(CubicBez::new((0., 950.), (175., 933.335), (341.665, 891.665), (525., 900.))), + PathSeg::Cubic(CubicBez::new((525., 900.), (708.335, 908.335), (908.335, 966.665), (1100., 1000.))), + ]; + + let vertical_segments = [ + PathSeg::Cubic(CubicBez::new((0., 0.), (-16.665, 150.), (-50., 291.665), (-50., 450.))), + PathSeg::Cubic(CubicBez::new((550., 50.), (558.335, 216.665), (579.165, 408.335), (575., 550.))), + PathSeg::Cubic(CubicBez::new((1100., 0.), (1116.665, 141.665), (1150., 258.335), (1150., 425.))), + PathSeg::Cubic(CubicBez::new((-50., 450.), (-50., 608.335), (-16.665, 783.335), (0., 950.))), + PathSeg::Cubic(CubicBez::new((575., 550.), (570.835, 691.665), (541.665, 783.335), (525., 900.))), + PathSeg::Cubic(CubicBez::new((1150., 425.), (1150., 591.665), (1116.665, 808.335), (1100., 1000.))), + ]; + + let sample_index = |row: isize, column: isize| -> usize { + let clamped_column = column.clamp(0, mesh_corner_count_column as isize - 1) as usize; + let clamped_row = row.clamp(0, mesh_corner_count_row as isize - 1) as usize; + clamped_row * mesh_corner_count_column + clamped_column + }; - // Split the mesh into 8 x 8 subgrids - let grid_num = 2; - let grid_stride_uv = 1. / grid_num as f64; + #[derive(Clone, Copy)] + struct MeshCornerDerivatives { + u: Vec4, + v: Vec4, + } - let segments: Vec<_> = vector.segment_iter().map(|(_, path_seg, _, _)| path_seg).collect(); - let points = vector.point_domain.positions(); + let gamma_colors: Vec = mesh_corner_colors.iter().map(|color| Vec4::from_array(color.to_gamma_srgb_channels())).collect(); + let calculate_derivative = |prev_index: usize, curr_index: usize, next_index: usize| { + let prev_color = gamma_colors[prev_index]; + let curr_color = gamma_colors[curr_index]; + let next_color = gamma_colors[next_index]; - let top_seg = segments[0]; - let right_seg = segments[1]; - let bottom_seg = segments[2]; - let left_seg = segments[3]; + let prev_distance = mesh_corner_pos[curr_index].distance(mesh_corner_pos[prev_index]) as f32; + let next_distance = mesh_corner_pos[next_index].distance(mesh_corner_pos[curr_index]) as f32; - let top_left_color = Color::RED; - let top_right_color = Color::BLUE; - let bottom_left_color = Color::GREEN; - let bottom_right_color = Color::YELLOW; + if prev_index == curr_index { + (next_color - curr_color) / next_distance + } else if next_index == curr_index { + (curr_color - prev_color) / prev_distance + } else { + let backward_diff = (curr_color - prev_color) / prev_distance; + let forward_diff = (next_color - curr_color) / next_distance; + let central_diff = (backward_diff + forward_diff) / 2.; + + // Prevent overshooting by using a zero slope at local minimum/maximum + // TODO: consider clamping slope by a constant value + Vec4::from_array(std::array::from_fn( + |channel| { + if backward_diff[channel] * forward_diff[channel] <= 0. { 0. } else { central_diff[channel] } + }, + )) + } + }; + + let mut mesh_corner_derivatives = Vec::with_capacity(mesh_corner_count_row * mesh_corner_count_column); + for row in 0..mesh_corner_count_row as isize { + for col in 0..mesh_corner_count_column as isize { + let curr_index = sample_index(row, col); + let u = calculate_derivative(sample_index(row, col - 1), curr_index, sample_index(row, col + 1)); + let v = calculate_derivative(sample_index(row - 1, col), curr_index, sample_index(row + 1, col)); + mesh_corner_derivatives.push(MeshCornerDerivatives { u, v }); + } + } + + let hermite = |a: f32, ma: f32, b: f32, mb: f32, t: f32| -> f32 { + let t_power_2 = t * t; + let t_power_3 = t_power_2 * t; + + let h1 = 2. * t_power_3 - 3. * t_power_2 + 1.; + let h2 = -2. * t_power_3 + 3. * t_power_2; + let h3 = t_power_3 - 2. * t_power_2 + t; + let h4 = t_power_3 - t_power_2; + + ma * h3 + a * h1 + b * h2 + mb * h4 + }; - 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 { @@ -1194,138 +1299,195 @@ impl Render for List { .to_rgba_hex() }; - type Row = Vec<(DVec2, [f32; 4])>; - type Grid = Vec; - - // 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) - }); + type SubpatchVertex = (DVec2, [f32; 4]); + type SubpatchVertexRow = Vec; + type SubpatchVertexRows = Vec; + + // Patch loop + for patch_v_index in 0..patch_count_row { + for patch_u_index in 0..patch_count_column { + let horizontal_segment_index = patch_v_index * patch_count_column + patch_u_index; + let vertical_segment_index = patch_v_index * (patch_count_column + 1) + patch_u_index; + + let tl_index = patch_v_index * mesh_corner_count_column + patch_u_index; + let tr_index = tl_index + 1; + let bl_index = (patch_v_index + 1) * mesh_corner_count_column + patch_u_index; + let br_index = bl_index + 1; + + let patch_top_seg = horizontal_segments[horizontal_segment_index]; + let patch_bottom_seg = horizontal_segments[horizontal_segment_index + patch_count_column]; + let patch_left_seg = vertical_segments[vertical_segment_index]; + let patch_right_seg = vertical_segments[vertical_segment_index + 1]; + + let tl_pos = mesh_corner_pos[tl_index]; + let tr_pos = mesh_corner_pos[tr_index]; + let bl_pos = mesh_corner_pos[bl_index]; + let br_pos = mesh_corner_pos[br_index]; + + let tl_color = mesh_corner_colors[tl_index]; + let tr_color = mesh_corner_colors[tr_index]; + let bl_color = mesh_corner_colors[bl_index]; + let br_color = mesh_corner_colors[br_index]; + + let tl_deriv = mesh_corner_derivatives[tl_index]; + let tr_deriv = mesh_corner_derivatives[tr_index]; + let bl_deriv = mesh_corner_derivatives[bl_index]; + let br_deriv = mesh_corner_derivatives[br_index]; + + // Store rows in increasing v order (bottom to top), with u increasing from left to right within each row. + let mut subpatch_vertex_rows: SubpatchVertexRows = vec![]; + + // Subpatch loop + for subpatch_vertex_v_index in 0..=subpatch_count_per_axis { + let v = subpatch_vertex_v_index as f64 * subpatch_stride_uv; + let mut subpatch_vertex_row: SubpatchVertexRow = vec![]; + for subpatch_vertex_u_index in 0..=subpatch_count_per_axis { + let u = subpatch_vertex_u_index as f64 * subpatch_stride_uv; + let top_u = point_to_dvec2(patch_top_seg.eval(u)); + let bottom_u = point_to_dvec2(patch_bottom_seg.eval(u)); + let left_v = point_to_dvec2(patch_left_seg.eval(v)); + let right_v = point_to_dvec2(patch_right_seg.eval(v)); + let s_c = (1. - v) * top_u + v * bottom_u; + let s_d = (1. - u) * left_v + u * right_v; + let s_b = mesh_corner_pos[tl_index] * (1. - u) * (1. - v) + + mesh_corner_pos[tr_index] * u * (1. - v) + + mesh_corner_pos[bl_index] * (1. - u) * v + + mesh_corner_pos[br_index] * u * v; + let position = mesh_transform.transform_point2(s_c + s_d - s_b); + + // We need to calculate the color for each subpatch vertex in 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 = bl_color.to_gamma_srgb_channels(); + let bottom_right_gamma = br_color.to_gamma_srgb_channels(); + let top_left_gamma = tl_color.to_gamma_srgb_channels(); + let top_right_gamma = tr_color.to_gamma_srgb_channels(); + + let top_length = tl_pos.distance(tr_pos) as f32; + let bottom_length = bl_pos.distance(br_pos) as f32; + let left_length = tl_pos.distance(bl_pos) as f32; + let right_length = tr_pos.distance(br_pos) as f32; + + // Calculate the colors for each subpatch corners by bicubic hermite interpolation. + // The cross derivatives are explicitly set to zero for the interpolation of the slopes. + // https://www.w3.org/TR/2015/WD-SVG2-20150915/pservers.html#MeshPatchPainting + let color_gamma: [f32; 4] = std::array::from_fn(|channel| { + let top_color_interpolated = hermite( + top_left_gamma[channel], + tl_deriv.u[channel] * top_length, + top_right_gamma[channel], + tr_deriv.u[channel] * top_length, + u as f32, + ); + let bottom_color_interpolated = hermite( + bottom_left_gamma[channel], + bl_deriv.u[channel] * bottom_length, + bottom_right_gamma[channel], + br_deriv.u[channel] * bottom_length, + u as f32, + ); + let top_slope_interpolated = hermite(tl_deriv.v[channel] * left_length, 0., tr_deriv.v[channel] * right_length, 0., u as f32); + let bottom_slope_interpolated = hermite(bl_deriv.v[channel] * left_length, 0., br_deriv.v[channel] * right_length, 0., u as f32); + hermite(top_color_interpolated, top_slope_interpolated, bottom_color_interpolated, bottom_slope_interpolated, v as f32) + }); + + // let color_gamma: [f32; 4] = std::array::from_fn(|channel| { + // top_left_gamma[channel] + // .lerp(top_right_gamma[channel], u as f32) + // .lerp(bottom_left_gamma[channel].lerp(bottom_right_gamma[channel], u as f32), v as f32) + // }); + + subpatch_vertex_row.push((position, color_gamma)); + } + subpatch_vertex_rows.push(subpatch_vertex_row); + } - grid_info_row.push((pos, color_gamma)); - } - grid_info.push(grid_info_row); - } + // Create polygons using the vertex position data + let mut idx = generate_uuid(); - // 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; - 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; - let patch_transform = format_transform_matrix(DAffine2::from_cols(tr - tl, bl - tl, tl)); - - // linear gradient for the bottom line - write!( + for subpatch_v_index in 0..subpatch_count_per_axis { + for subpatch_u_index in 0..subpatch_count_per_axis { + let (bl, bl_color) = subpatch_vertex_rows[subpatch_v_index][subpatch_u_index]; + let (br, br_color) = subpatch_vertex_rows[subpatch_v_index][subpatch_u_index + 1]; + let (tl, tl_color) = subpatch_vertex_rows[subpatch_v_index + 1][subpatch_u_index]; + let (tr, tr_color) = subpatch_vertex_rows[subpatch_v_index + 1][subpatch_u_index + 1]; + let subpatch_transform = format_transform_matrix(DAffine2::from_cols(tr - tl, bl - tl, tl)); + + // linear gradient for the bottom line + write!( &mut render.svg_defs, - r##""##, + r##""##, float_srgba_to_hex(bl_color), float_srgba_to_hex(br_color), ) .unwrap(); - // linear gradient for the top line - write!( + // linear gradient for the top line + write!( &mut render.svg_defs, - r##""##, + r##""##, float_srgba_to_hex(tl_color), float_srgba_to_hex(tr_color), ) .unwrap(); - // top mask gradient - write!( + // top mask gradient + write!( &mut render.svg_defs, - r##""##, + r##""##, ) .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##" + // Inflate a subpatch 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##" "##, - ) - .unwrap(); + ) + .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(#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})")); - }); + 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; + 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; } From 84ba0a7c4e33bafd50414eb6a86e5672a8d826a5 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Tue, 14 Jul 2026 15:33:50 +0900 Subject: [PATCH 5/8] Introduces mesh gradient related structs --- .../data_panel/data_panel_message_handler.rs | 19 +- .../libraries/graphic-types/src/graphic.rs | 20 +- .../libraries/rendering/src/render_ext.rs | 4 +- .../libraries/rendering/src/renderer.rs | 480 ++++++------------ .../libraries/vector-types/src/gradient.rs | 408 ++++++++++++++- .../src/vector/vector_attributes.rs | 9 + node-graph/nodes/path-bool/src/lib.rs | 2 + 7 files changed, 600 insertions(+), 342 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 908e883892..f0966637b7 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -23,7 +23,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType}; use graphene_std::vector::misc::{ ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType, }; -use graphene_std::vector::style::{DashPattern, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{DashPattern, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType, MeshGradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector}; use graphene_std::{Artboard, Color, Context, Graphic}; use std::any::Any; @@ -518,6 +518,7 @@ impl TableItemLayout for Graphic { Self::RasterGPU(list) => list.identifier(), Self::Color(list) => list.identifier(), Self::Gradient(list) => list.identifier(), + Self::MeshGradient(list) => list.identifier(), Self::Text(list) => list.identifier(), } } @@ -534,6 +535,7 @@ impl TableItemLayout for Graphic { Self::RasterGPU(list) => list.layout_with_breadcrumb(data), Self::Color(list) => list.layout_with_breadcrumb(data), Self::Gradient(list) => list.layout_with_breadcrumb(data), + Self::MeshGradient(list) => list.layout_with_breadcrumb(data), Self::Text(list) => list.layout_with_breadcrumb(data), } } @@ -740,6 +742,21 @@ impl TableItemLayout for Gradient { } } +impl TableItemLayout for MeshGradient { + fn type_name() -> &'static str { + "MeshGradient" + } + fn identifier(&self) -> String { + //FIXME: not implemented yet + format!("MeshGradient ({})", 1) + } + fn value_page(&self, _data: &mut LayoutData) -> Vec { + //FIXME: not implemented yet + let widgets = vec![TextLabel::new("FIXME: MeshGradient cannot be displayed here").widget_instance()]; + vec![LayoutGroup::row(widgets)] + } +} + impl TableItemLayout for f64 { fn type_name() -> &'static str { "Number (f64)" diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index e5197f8e55..e3494c5c23 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -10,6 +10,7 @@ use raster_types::{CPU, GPU, Raster}; use std::borrow::Cow; use vector_types::Gradient; pub use vector_types::Vector; +use vector_types::gradient::MeshGradient; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. #[derive(Clone, Debug, Default, CacheHash, PartialEq, DynAny)] @@ -23,6 +24,7 @@ pub enum Graphic { RasterGPU(List>), Color(List), Gradient(List), + MeshGradient(List), Text(List), } @@ -234,6 +236,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA Graphic::RasterCPU(list) => bake_list_transform(list, transform), Graphic::RasterGPU(list) => bake_list_transform(list, transform), Graphic::Gradient(list) => bake_list_transform(list, transform), + Graphic::MeshGradient(list) => bake_list_transform(list, transform), Graphic::Text(list) => bake_list_transform(list, transform), Graphic::Color(_) => {} } @@ -339,6 +342,12 @@ impl IntoGraphicList for List { } } +impl IntoGraphicList for List { + fn into_graphic_list(self) -> List { + List::new_from_element(Graphic::MeshGradient(self)) + } +} + impl IntoGraphicList for List { fn into_graphic_list(self) -> List { let layer_path = self.attribute::(ATTR_EDITOR_LAYER_PATH, 0).cloned(); @@ -427,6 +436,7 @@ impl Graphic { Graphic::RasterGPU(list) => all_clipped(list), Graphic::Color(list) => all_clipped(list), Graphic::Gradient(list) => all_clipped(list), + Graphic::MeshGradient(list) => all_clipped(list), Graphic::Text(list) => all_clipped(list), } } @@ -467,7 +477,8 @@ impl Graphic { } Graphic::Color(list) => list.element(0).is_some_and(|color| color.is_opaque()), Graphic::Gradient(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.is_opaque())), - Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false, + // TODO: Graphic::MeshGradient should be able to have this check + Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) | Graphic::MeshGradient(_) => false, } } @@ -491,7 +502,8 @@ impl Graphic { }), Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.), Graphic::Gradient(list) => list.iter_element_values().all(|stops| stops.iter().all(|stop| stop.color.a() == 0.)), - Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false, + // TODO: Graphic::MeshGradient should be able to have this check + Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) | Graphic::MeshGradient(_) => false, } } @@ -509,6 +521,7 @@ impl Graphic { Graphic::Vector(list) => list.is_empty(), Graphic::Color(list) => list.is_empty(), Graphic::Gradient(list) => list.is_empty(), + Graphic::MeshGradient(list) => list.is_empty(), Graphic::RasterCPU(list) => list.is_empty(), Graphic::RasterGPU(list) => list.is_empty(), Graphic::Text(list) => list.is_empty(), @@ -526,6 +539,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(list) => list.bounding_box(transform, include_stroke), Graphic::Color(list) => list.bounding_box(transform, include_stroke), Graphic::Gradient(list) => list.bounding_box(transform, include_stroke), + Graphic::MeshGradient(list) => list.bounding_box(transform, include_stroke), Graphic::Text(list) => list.bounding_box(transform, include_stroke), } } @@ -539,6 +553,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke), Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke), + Graphic::MeshGradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke), Graphic::Text(list) => list.thumbnail_bounding_box(transform, include_stroke), } } @@ -554,6 +569,7 @@ impl RenderComplexity for Graphic { Self::RasterGPU(list) => list.render_complexity(), Self::Color(list) => list.render_complexity(), Self::Gradient(list) => list.render_complexity(), + Self::MeshGradient(list) => list.render_complexity(), Self::Text(list) => list.render_complexity(), } } diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index 9d2ef67b0c..6f72314aee 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -242,7 +242,7 @@ impl RenderExt for List { format!(r##" {paint_attr}="url(#{gradient_id})""##) } Some(Graphic::None) => format!(r#" {paint_attr}="none""#), - Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) => { + Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::Graphic(_)) | Some(Graphic::Text(_)) | Some(Graphic::MeshGradient(_)) => { let bounds = if target == PaintTarget::Stroke { // To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly. let inverse = |len: f64| if len > 0. { 1. / len } else { 0. }; @@ -263,7 +263,7 @@ impl RenderExt for List { } /// Emits an SVG `` paint server into `svg_defs` that renders the given graphic list as the paint content, and returns the pattern ID. -/// Currently, this function is only used for clipping-based filling and stroking, not considering tiling yet. +/// Currently, this function is only used for clipping-based filling and stroking and mesh gradient, not considering tiling yet. fn render_svg_pattern(svg_defs: &mut String, fill_graphic_list: &List, stroke_transform: DAffine2, bounds: DAffine2, render_params: &RenderParams) -> Option { let min = bounds.transform_point2(DVec2::ZERO); let max = bounds.transform_point2(DVec2::ONE); diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index b1950da762..d76560c13d 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -16,7 +16,7 @@ use core_types::{ ATTR_TEXT_ALIGN, ATTR_TRANSFORM, }; use dyn_any::DynAny; -use glam::{DAffine2, DMat2, DVec2, FloatExt, Vec4}; +use glam::{DAffine2, DMat2, DVec2, Vec4}; use graphene_hash::CacheHashWrapper; use graphene_resource::Resource; use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute}; @@ -27,7 +27,7 @@ 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, CubicBez, Join, ParamCurve, PathSeg, Shape, StrokeOpts}; -use num_traits::{Float, Zero}; +use num_traits::Zero; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; use skrifa::raw::FontRef as SkrifaFontRef; @@ -35,9 +35,9 @@ use skrifa::{GlyphId, MetadataProvider}; use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::hash::Hash; -use std::ops::{Add, Deref, Mul, Sub}; +use std::ops::Deref; use std::sync::{Arc, LazyLock}; -use vector_types::gradient::GradientSpreadMethod; +use vector_types::gradient::{GradientSpreadMethod, MeshGradient}; use vector_types::vector::misc::point_to_dvec2; use vello::*; @@ -552,6 +552,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(list) => list.render_svg(render, render_params), Graphic::Gradient(list) => list.render_svg(render, render_params), + Graphic::MeshGradient(list) => list.render_svg(render, render_params), Graphic::Text(list) => list.render_svg(render, render_params), } } @@ -565,6 +566,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Color(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Gradient(list) => list.render_to_vello(scene, transform, context, render_params), + Graphic::MeshGradient(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Text(list) => list.render_to_vello(scene, transform, context, render_params), } } @@ -620,6 +622,14 @@ impl Render for Graphic { metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); } } + Graphic::MeshGradient(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + } + } Graphic::Text(list) => { metadata.upstream_footprints.insert(element_id, footprint); @@ -639,6 +649,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.collect_metadata(metadata, footprint, element_id), Graphic::Color(list) => list.collect_metadata(metadata, footprint, element_id), Graphic::Gradient(list) => list.collect_metadata(metadata, footprint, element_id), + Graphic::MeshGradient(list) => list.collect_metadata(metadata, footprint, element_id), Graphic::Text(list) => list.collect_metadata(metadata, footprint, element_id), } } @@ -652,6 +663,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.add_upstream_click_targets(click_targets), Graphic::Color(list) => list.add_upstream_click_targets(click_targets), Graphic::Gradient(list) => list.add_upstream_click_targets(click_targets), + Graphic::MeshGradient(list) => list.add_upstream_click_targets(click_targets), Graphic::Text(list) => list.add_upstream_click_targets(click_targets), } } @@ -665,6 +677,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.add_upstream_outline_targets(outlines), Graphic::Color(list) => list.add_upstream_outline_targets(outlines), Graphic::Gradient(list) => list.add_upstream_outline_targets(outlines), + Graphic::MeshGradient(list) => list.add_upstream_outline_targets(outlines), Graphic::Text(list) => list.add_upstream_outline_targets(outlines), } } @@ -678,6 +691,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.contains_artboard(), Graphic::Color(list) => list.contains_artboard(), Graphic::Gradient(list) => list.contains_artboard(), + Graphic::MeshGradient(list) => list.contains_artboard(), Graphic::Text(list) => list.contains_artboard(), } } @@ -691,6 +705,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(_) => (), Graphic::Gradient(_) => (), + Graphic::MeshGradient(_) => (), Graphic::Text(_) => (), } } @@ -1159,338 +1174,6 @@ impl Render for List { } } - 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::(ATTR_GRADIENT_TYPE, 0) == GradientType::Mesh)); - if is_mesh_fill { - let mesh_transform = element_transform * applied_stroke_transform; - let mesh_corner_count_column: usize = 3; - let mesh_corner_count_row: usize = 3; - let patch_count_column = mesh_corner_count_column - 1; - let patch_count_row = mesh_corner_count_row - 1; - let subpatch_count_per_patch = 4; - let subpatch_count_per_axis = patch_count_row * subpatch_count_per_patch; - let subpatch_stride_uv = 1. / subpatch_count_per_axis as f64; - - let top_left_color = Color::BLACK; - let top_middle_color = Color::WHITE; - let top_right_color = Color::BLACK; - - let middle_left_color = Color::WHITE; - let center_color = Color::BLACK; - let middle_right_color = Color::WHITE; - - let bottom_left_color = Color::BLACK; - let bottom_middle_color = Color::WHITE; - let bottom_right_color = Color::BLACK; - - let mesh_corner_colors = vec![ - bottom_left_color, - bottom_middle_color, - bottom_right_color, - middle_left_color, - center_color, - middle_right_color, - top_left_color, - top_middle_color, - top_right_color, - ]; - - let mesh_corner_pos = vec![ - DVec2::new(0., 0.), - DVec2::new(550., 50.), - DVec2::new(1100., 0.), - DVec2::new(-50., 450.), - DVec2::new(575., 550.), - DVec2::new(1150., 425.), - DVec2::new(0., 950.), - DVec2::new(525., 900.), - DVec2::new(1100., 1000.), - ]; - - let horizontal_segments = [ - PathSeg::Cubic(CubicBez::new((0., 0.), (183.335, 16.665), (366.665, 50.), (550., 50.))), - PathSeg::Cubic(CubicBez::new((550., 50.), (733.335, 50.), (916.665, 16.665), (1100., 0.))), - PathSeg::Cubic(CubicBez::new((-50., 450.), (158.335, 483.335), (375., 554.165), (575., 550.))), - PathSeg::Cubic(CubicBez::new((575., 550.), (775., 545.835), (958.335, 466.665), (1150., 425.))), - PathSeg::Cubic(CubicBez::new((0., 950.), (175., 933.335), (341.665, 891.665), (525., 900.))), - PathSeg::Cubic(CubicBez::new((525., 900.), (708.335, 908.335), (908.335, 966.665), (1100., 1000.))), - ]; - - let vertical_segments = [ - PathSeg::Cubic(CubicBez::new((0., 0.), (-16.665, 150.), (-50., 291.665), (-50., 450.))), - PathSeg::Cubic(CubicBez::new((550., 50.), (558.335, 216.665), (579.165, 408.335), (575., 550.))), - PathSeg::Cubic(CubicBez::new((1100., 0.), (1116.665, 141.665), (1150., 258.335), (1150., 425.))), - PathSeg::Cubic(CubicBez::new((-50., 450.), (-50., 608.335), (-16.665, 783.335), (0., 950.))), - PathSeg::Cubic(CubicBez::new((575., 550.), (570.835, 691.665), (541.665, 783.335), (525., 900.))), - PathSeg::Cubic(CubicBez::new((1150., 425.), (1150., 591.665), (1116.665, 808.335), (1100., 1000.))), - ]; - - let sample_index = |row: isize, column: isize| -> usize { - let clamped_column = column.clamp(0, mesh_corner_count_column as isize - 1) as usize; - let clamped_row = row.clamp(0, mesh_corner_count_row as isize - 1) as usize; - clamped_row * mesh_corner_count_column + clamped_column - }; - - #[derive(Clone, Copy)] - struct MeshCornerDerivatives { - u: Vec4, - v: Vec4, - } - - let gamma_colors: Vec = mesh_corner_colors.iter().map(|color| Vec4::from_array(color.to_gamma_srgb_channels())).collect(); - let calculate_derivative = |prev_index: usize, curr_index: usize, next_index: usize| { - let prev_color = gamma_colors[prev_index]; - let curr_color = gamma_colors[curr_index]; - let next_color = gamma_colors[next_index]; - - let prev_distance = mesh_corner_pos[curr_index].distance(mesh_corner_pos[prev_index]) as f32; - let next_distance = mesh_corner_pos[next_index].distance(mesh_corner_pos[curr_index]) as f32; - - if prev_index == curr_index { - (next_color - curr_color) / next_distance - } else if next_index == curr_index { - (curr_color - prev_color) / prev_distance - } else { - let backward_diff = (curr_color - prev_color) / prev_distance; - let forward_diff = (next_color - curr_color) / next_distance; - let central_diff = (backward_diff + forward_diff) / 2.; - - // Prevent overshooting by using a zero slope at local minimum/maximum - // TODO: consider clamping slope by a constant value - Vec4::from_array(std::array::from_fn( - |channel| { - if backward_diff[channel] * forward_diff[channel] <= 0. { 0. } else { central_diff[channel] } - }, - )) - } - }; - - let mut mesh_corner_derivatives = Vec::with_capacity(mesh_corner_count_row * mesh_corner_count_column); - for row in 0..mesh_corner_count_row as isize { - for col in 0..mesh_corner_count_column as isize { - let curr_index = sample_index(row, col); - let u = calculate_derivative(sample_index(row, col - 1), curr_index, sample_index(row, col + 1)); - let v = calculate_derivative(sample_index(row - 1, col), curr_index, sample_index(row + 1, col)); - mesh_corner_derivatives.push(MeshCornerDerivatives { u, v }); - } - } - - let hermite = |a: f32, ma: f32, b: f32, mb: f32, t: f32| -> f32 { - let t_power_2 = t * t; - let t_power_3 = t_power_2 * t; - - let h1 = 2. * t_power_3 - 3. * t_power_2 + 1.; - let h2 = -2. * t_power_3 + 3. * t_power_2; - let h3 = t_power_3 - 2. * t_power_2 + t; - let h4 = t_power_3 - t_power_2; - - ma * h3 + a * h1 + b * h2 + mb * h4 - }; - - 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 SubpatchVertex = (DVec2, [f32; 4]); - type SubpatchVertexRow = Vec; - type SubpatchVertexRows = Vec; - - // Patch loop - for patch_v_index in 0..patch_count_row { - for patch_u_index in 0..patch_count_column { - let horizontal_segment_index = patch_v_index * patch_count_column + patch_u_index; - let vertical_segment_index = patch_v_index * (patch_count_column + 1) + patch_u_index; - - let tl_index = patch_v_index * mesh_corner_count_column + patch_u_index; - let tr_index = tl_index + 1; - let bl_index = (patch_v_index + 1) * mesh_corner_count_column + patch_u_index; - let br_index = bl_index + 1; - - let patch_top_seg = horizontal_segments[horizontal_segment_index]; - let patch_bottom_seg = horizontal_segments[horizontal_segment_index + patch_count_column]; - let patch_left_seg = vertical_segments[vertical_segment_index]; - let patch_right_seg = vertical_segments[vertical_segment_index + 1]; - - let tl_pos = mesh_corner_pos[tl_index]; - let tr_pos = mesh_corner_pos[tr_index]; - let bl_pos = mesh_corner_pos[bl_index]; - let br_pos = mesh_corner_pos[br_index]; - - let tl_color = mesh_corner_colors[tl_index]; - let tr_color = mesh_corner_colors[tr_index]; - let bl_color = mesh_corner_colors[bl_index]; - let br_color = mesh_corner_colors[br_index]; - - let tl_deriv = mesh_corner_derivatives[tl_index]; - let tr_deriv = mesh_corner_derivatives[tr_index]; - let bl_deriv = mesh_corner_derivatives[bl_index]; - let br_deriv = mesh_corner_derivatives[br_index]; - - // Store rows in increasing v order (bottom to top), with u increasing from left to right within each row. - let mut subpatch_vertex_rows: SubpatchVertexRows = vec![]; - - // Subpatch loop - for subpatch_vertex_v_index in 0..=subpatch_count_per_axis { - let v = subpatch_vertex_v_index as f64 * subpatch_stride_uv; - let mut subpatch_vertex_row: SubpatchVertexRow = vec![]; - for subpatch_vertex_u_index in 0..=subpatch_count_per_axis { - let u = subpatch_vertex_u_index as f64 * subpatch_stride_uv; - let top_u = point_to_dvec2(patch_top_seg.eval(u)); - let bottom_u = point_to_dvec2(patch_bottom_seg.eval(u)); - let left_v = point_to_dvec2(patch_left_seg.eval(v)); - let right_v = point_to_dvec2(patch_right_seg.eval(v)); - let s_c = (1. - v) * top_u + v * bottom_u; - let s_d = (1. - u) * left_v + u * right_v; - let s_b = mesh_corner_pos[tl_index] * (1. - u) * (1. - v) - + mesh_corner_pos[tr_index] * u * (1. - v) - + mesh_corner_pos[bl_index] * (1. - u) * v - + mesh_corner_pos[br_index] * u * v; - let position = mesh_transform.transform_point2(s_c + s_d - s_b); - - // We need to calculate the color for each subpatch vertex in 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 = bl_color.to_gamma_srgb_channels(); - let bottom_right_gamma = br_color.to_gamma_srgb_channels(); - let top_left_gamma = tl_color.to_gamma_srgb_channels(); - let top_right_gamma = tr_color.to_gamma_srgb_channels(); - - let top_length = tl_pos.distance(tr_pos) as f32; - let bottom_length = bl_pos.distance(br_pos) as f32; - let left_length = tl_pos.distance(bl_pos) as f32; - let right_length = tr_pos.distance(br_pos) as f32; - - // Calculate the colors for each subpatch corners by bicubic hermite interpolation. - // The cross derivatives are explicitly set to zero for the interpolation of the slopes. - // https://www.w3.org/TR/2015/WD-SVG2-20150915/pservers.html#MeshPatchPainting - let color_gamma: [f32; 4] = std::array::from_fn(|channel| { - let top_color_interpolated = hermite( - top_left_gamma[channel], - tl_deriv.u[channel] * top_length, - top_right_gamma[channel], - tr_deriv.u[channel] * top_length, - u as f32, - ); - let bottom_color_interpolated = hermite( - bottom_left_gamma[channel], - bl_deriv.u[channel] * bottom_length, - bottom_right_gamma[channel], - br_deriv.u[channel] * bottom_length, - u as f32, - ); - let top_slope_interpolated = hermite(tl_deriv.v[channel] * left_length, 0., tr_deriv.v[channel] * right_length, 0., u as f32); - let bottom_slope_interpolated = hermite(bl_deriv.v[channel] * left_length, 0., br_deriv.v[channel] * right_length, 0., u as f32); - hermite(top_color_interpolated, top_slope_interpolated, bottom_color_interpolated, bottom_slope_interpolated, v as f32) - }); - - // let color_gamma: [f32; 4] = std::array::from_fn(|channel| { - // top_left_gamma[channel] - // .lerp(top_right_gamma[channel], u as f32) - // .lerp(bottom_left_gamma[channel].lerp(bottom_right_gamma[channel], u as f32), v as f32) - // }); - - subpatch_vertex_row.push((position, color_gamma)); - } - subpatch_vertex_rows.push(subpatch_vertex_row); - } - - // Create polygons using the vertex position data - let mut idx = generate_uuid(); - - for subpatch_v_index in 0..subpatch_count_per_axis { - for subpatch_u_index in 0..subpatch_count_per_axis { - let (bl, bl_color) = subpatch_vertex_rows[subpatch_v_index][subpatch_u_index]; - let (br, br_color) = subpatch_vertex_rows[subpatch_v_index][subpatch_u_index + 1]; - let (tl, tl_color) = subpatch_vertex_rows[subpatch_v_index + 1][subpatch_u_index]; - let (tr, tr_color) = subpatch_vertex_rows[subpatch_v_index + 1][subpatch_u_index + 1]; - let subpatch_transform = format_transform_matrix(DAffine2::from_cols(tr - tl, bl - tl, tl)); - - // linear gradient for the bottom line - write!( - &mut render.svg_defs, - r##""##, - 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##""##, - float_srgba_to_hex(tl_color), - float_srgba_to_hex(tr_color), - ) - .unwrap(); - - // top mask gradient - write!( - &mut render.svg_defs, - r##""##, - ) - .unwrap(); - - // Inflate a subpatch 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##" - "##, - ) - .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; - } - } - } - } - return; - } - render.leaf_tag("path", |attributes| { attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); @@ -1712,6 +1395,9 @@ impl Render for List { paint.render_to_vello(scene, multiplied_transform, context, render_params); scene.pop_layer(); } + Graphic::MeshGradient(_) => { + // FIXME: implement vello rendering + } }; } }; @@ -1790,6 +1476,9 @@ impl Render for List { scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, Some(brush_transform), &path); } + Graphic::MeshGradient(_) => { + // FIXME: implement vello rendering + } Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) => { let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01); @@ -2568,6 +2257,125 @@ impl Render for List { } } +impl Render for List { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + for index in 0..self.len() { + let Some(mesh_gradient) = self.element(index) else { continue }; + let mesh_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + // FIXME: use below attrs + let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); + let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); + let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + + // TODO: add adaptive subpatch size + let subdivisions_per_patch_per_axis = 4; + + let float_gamma_color_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() + }; + + let Some(subpatches) = mesh_gradient + .evaluator() + .and_then(|evaluator| evaluator.subdivide_patches(subdivisions_per_patch_per_axis, mesh_transform)) + else { + continue; + }; + + let mut unique_id = generate_uuid(); + subpatches.iter().for_each(|subpatch| { + let [top_left, top_right, bottom_left, bottom_right] = subpatch.corners; + let subpatch_transform = format_transform_matrix(DAffine2::from_cols(top_right.position - top_left.position, bottom_left.position - top_left.position, top_left.position)); + + // linear gradient for the bottom line + write!( + &mut render.svg_defs, + r##""##, + float_gamma_color_to_hex(bottom_left.gamma_color), + float_gamma_color_to_hex(bottom_right.gamma_color), + ) + .unwrap(); + + // linear gradient for the top line + write!( + &mut render.svg_defs, + r##""##, + float_gamma_color_to_hex(top_left.gamma_color), + float_gamma_color_to_hex(top_right.gamma_color), + ) + .unwrap(); + + // top mask gradient + write!( + &mut render.svg_defs, + r##""##, + ) + .unwrap(); + + // Inflate the subpatch to hide gaps caused by anti-aliasing + let inflation_amount = 1.; + + let top_normal = (top_left.position - top_right.position).perp().normalize(); + let bottom_normal = (bottom_right.position - bottom_left.position).perp().normalize(); + let left_normal = (bottom_left.position - top_left.position).perp().normalize(); + let right_normal = (top_right.position - bottom_right.position).perp().normalize(); + + let inflated_top_left_pos = top_left.position + (top_normal + left_normal) * inflation_amount; + let inflated_top_right_pos = top_right.position + (top_normal + right_normal) * inflation_amount; + let inflated_bottom_left_pos = bottom_left.position + (bottom_normal + left_normal) * inflation_amount; + let inflated_bottom_right_pos = bottom_right.position + (bottom_normal + right_normal) * inflation_amount; + + let DVec2 { x: top_left_x, y: top_left_y } = inflated_top_left_pos; + let DVec2 { x: top_right_x, y: top_right_y } = inflated_top_right_pos; + let DVec2 { x: bottom_left_x, y: bottom_left_y } = inflated_bottom_left_pos; + let DVec2 { x: bottom_right_x, y: bottom_right_y } = inflated_bottom_right_pos; + + // mask + let min_x = bottom_left_x.min(top_left_x.min(bottom_right_x.min(top_right_x))); + let max_x = bottom_left_x.max(top_left_x.max(bottom_right_x.max(top_right_x))); + let min_y = bottom_left_y.min(top_left_y.min(bottom_right_y.min(top_right_y))); + let max_y = bottom_left_y.max(top_left_y.max(bottom_right_y.max(top_right_y))); + let mask_width = max_x - min_x; + let mask_height = max_y - min_y; + write!( + &mut render.svg_defs, + r##" + "##, + ) + .unwrap(); + + render.leaf_tag("polygon", |attributes| { + attributes.push("points", format!("{bottom_left_x},{bottom_left_y} {bottom_right_x},{bottom_right_y} {top_right_x},{top_right_y} {top_left_x},{top_left_y}")); + attributes.push("fill", format!("url(#gb{unique_id})")); + }); + + render.leaf_tag("polygon", |attributes| { + attributes.push("points", format!("{bottom_left_x},{bottom_left_y} {bottom_right_x},{bottom_right_y} {top_right_x},{top_right_y} {top_left_x},{top_left_y}")); + attributes.push("fill", format!("url(#gt{unique_id})")); + attributes.push("mask", format!("url(#m{unique_id})")); + }); + + unique_id += 1; + }); + } + } + + fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { + // FIXME: + } +} + /// Builds a `kurbo::BezPath` from a glyph outline, baking in the glyph origin (`ox`, `oy`) and faux-italic shear (`tilt_tan`). struct GlyphOutlinePen<'a> { path: &'a mut BezPath, diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 1de108c660..7ca2a6135f 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -2,7 +2,13 @@ use core_types::Color; use core_types::color::SRGBA8; use core_types::render_complexity::RenderComplexity; use dyn_any::DynAny; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DVec2, Vec4}; +use kurbo::{ParamCurve, PathSeg}; + +use crate::{ + Vector, + vector::{PointId, SegmentId, misc::point_to_dvec2}, +}; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] @@ -492,6 +498,392 @@ impl GradientSpreadMethod { } } +/// Resolved patch of a mesh gradient. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MeshPatch { + /// Corner positions. [top-left, top-right, bottom-left, bottom-right] + pub corners: [DVec2; 4], + /// Corner colors. [top-left, top-right, bottom-left, bottom-right] + pub colors: [Color; 4], + /// Edges defining the patch. [top, bottom, left, right] + pub edges: [PathSeg; 4], +} + +/// Definition of a mesh gradient patch. The entity is stored in the corresponding `MeshGradient` struct. +#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +struct MeshPatchDefinition { + corner_indices: [usize; 4], + edges: [SegmentId; 4], +} + +/// Mesh gradient defined by multiple coons patches. +#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct MeshGradient { + mesh_geometry: Vector, + pub corner_rows: usize, + pub corner_columns: usize, + /// Flatten list of corner's point IDs, row-major order. + corner_points: Vec, + /// Flatten list of corner colors, row-major order. + corner_colors: Vec, + /// Flatten list of mesh patches, row-major order. + patch_defs: Vec, +} + +impl MeshGradient { + fn resolve_patch(&self, patch_def: &MeshPatchDefinition) -> Option { + // Normalizes the orientation of the patch edge. + let resolve_oriented_edge = |segment_id: SegmentId, expected_start: PointId, expected_end: PointId| -> Option { + let (actual_start, actual_end, _) = self.mesh_geometry.segment_points_from_id(segment_id)?; + let segment = self.mesh_geometry.path_segment_from_id(segment_id)?; + + if actual_start == expected_start && actual_end == expected_end { + Some(segment) + } else if actual_start == expected_end && actual_end == expected_start { + Some(segment.reverse()) + } else { + // Segment is not connected to the expected patch corners. + None + } + }; + + let [top_left_index, top_right_index, bottom_left_index, bottom_right_index] = patch_def.corner_indices; + + let top_left_id = *self.corner_points.get(top_left_index)?; + let top_right_id = *self.corner_points.get(top_right_index)?; + let bottom_left_id = *self.corner_points.get(bottom_left_index)?; + let bottom_right_id = *self.corner_points.get(bottom_right_index)?; + + let corners = [ + self.mesh_geometry.point_domain.position_from_id(top_left_id)?, + self.mesh_geometry.point_domain.position_from_id(top_right_id)?, + self.mesh_geometry.point_domain.position_from_id(bottom_left_id)?, + self.mesh_geometry.point_domain.position_from_id(bottom_right_id)?, + ]; + + let colors = [ + *self.corner_colors.get(top_left_index)?, + *self.corner_colors.get(top_right_index)?, + *self.corner_colors.get(bottom_left_index)?, + *self.corner_colors.get(bottom_right_index)?, + ]; + + let [top_edge_id, bottom_edge_id, left_edge_id, right_edge_id] = patch_def.edges; + + let edges = [ + resolve_oriented_edge(top_edge_id, top_left_id, top_right_id)?, + resolve_oriented_edge(bottom_edge_id, bottom_left_id, bottom_right_id)?, + resolve_oriented_edge(left_edge_id, top_left_id, bottom_left_id)?, + resolve_oriented_edge(right_edge_id, top_right_id, bottom_right_id)?, + ]; + + Some(MeshPatch { corners, colors, edges }) + } + + // Returns resolved patch by the provided row/column position, if any. + fn patch(&self, row: usize, column: usize) -> Option { + let patch_rows = self.corner_rows.checked_sub(1)?; + let patch_columns = self.corner_columns.checked_sub(1)?; + + if row >= patch_rows || column >= patch_columns { + return None; + } + + let patch_columns = self.corner_columns.saturating_sub(1); + let patch_index = row.checked_mul(patch_columns)?.checked_add(column)?; + let patch_def = self.patch_defs.get(patch_index)?; + + self.resolve_patch(patch_def) + } + + pub fn patches(&self) -> impl Iterator> + '_ { + let patch_rows = self.corner_rows.saturating_sub(1); + let patch_columns = self.corner_columns.saturating_sub(1); + (0..patch_rows).flat_map(move |row| (0..patch_columns).map(move |column| self.patch(row, column))) + } + + pub fn evaluator(&self) -> Option { + MeshGradientEvaluator::new(self) + } +} + +/// Single vertex of a subpatch. Only for rendering purpose. +#[derive(Clone, Copy)] + +pub struct MeshSubpatchVertex { + pub position: DVec2, + pub gamma_color: [f32; 4], +} + +pub struct MeshSubpatch { + pub corners: [MeshSubpatchVertex; 4], +} + +#[derive(Clone, Copy)] +struct MeshCornerDerivatives { + u: Vec4, + v: Vec4, +} + +/// A cached mesh patch for subdivision into subpatches in rendering phase. +#[derive(Clone, Copy)] +struct MeshPatchEvaluator { + /// Corner positions. [top-left, top-right, bottom-left, bottom-right] + pub corners: [DVec2; 4], + /// Edges defining the patch. [top, bottom, left, right] + pub edges: [PathSeg; 4], + // sRGB gamma space color in 0.-1. [top-left, top-right, bottom-left, bottom-right] + gamma_colors: [Vec4; 4], + /// Slopes of corner colors for bicubic hermite interpolation. [top-left, top-right, bottom-left, bottom-right] + color_slopes: [MeshCornerDerivatives; 4], + /// Linear length of between each corner. [top, bottom, left, right] + lengths: [f32; 4], +} + +impl MeshPatchEvaluator { + /// Evaluate interpolated color in a mesh gradient's patch using bicubic hermite interpolation. + fn eval_color(&self, u: f32, v: f32) -> Option<[f32; 4]> { + if !(0. ..=1.).contains(&u) || !(0. ..=1.).contains(&v) { + return None; + } + + let hermite = |a: f32, ma: f32, b: f32, mb: f32, t: f32| -> f32 { + let t_power_2 = t * t; + let t_power_3 = t_power_2 * t; + + let h1 = 2. * t_power_3 - 3. * t_power_2 + 1.; + let h2 = -2. * t_power_3 + 3. * t_power_2; + let h3 = t_power_3 - 2. * t_power_2 + t; + let h4 = t_power_3 - t_power_2; + + ma * h3 + a * h1 + b * h2 + mb * h4 + }; + + let [top_left_gamma, top_right_gamma, bottom_left_gamma, bottom_right_gamma] = self.gamma_colors; + let [top_length, bottom_length, left_length, right_length] = self.lengths; + let [top_left_color_slope, top_right_color_slope, bottom_left_color_slope, bottom_right_color_slope] = self.color_slopes; + + let interpolated_gamma_color: [f32; 4] = std::array::from_fn(|channel| { + let top_color_interpolated = hermite( + top_left_gamma[channel], + top_left_color_slope.u[channel] * top_length, + top_right_gamma[channel], + top_right_color_slope.u[channel] * top_length, + u, + ); + let bottom_color_interpolated = hermite( + bottom_left_gamma[channel], + bottom_left_color_slope.u[channel] * bottom_length, + bottom_right_gamma[channel], + bottom_right_color_slope.u[channel] * bottom_length, + u, + ); + let top_slope_interpolated = hermite(top_left_color_slope.v[channel] * left_length, 0., top_right_color_slope.v[channel] * right_length, 0., u); + let bottom_slope_interpolated = hermite(bottom_left_color_slope.v[channel] * left_length, 0., bottom_right_color_slope.v[channel] * right_length, 0., u); + hermite(top_color_interpolated, top_slope_interpolated, bottom_color_interpolated, bottom_slope_interpolated, v) + }); + + Some(interpolated_gamma_color) + } + + fn eval_vertex(&self, u: f64, v: f64, mesh_transform: DAffine2) -> Option { + let [top_seg, bottom_seg, left_seg, right_seg] = self.edges; + let [top_left, top_right, bottom_left, bottom_right] = self.corners; + + let top_u = point_to_dvec2(top_seg.eval(u)); + let bottom_u = point_to_dvec2(bottom_seg.eval(u)); + let left_v = point_to_dvec2(left_seg.eval(v)); + let right_v = point_to_dvec2(right_seg.eval(v)); + + let s_c = (1. - v) * top_u + v * bottom_u; + let s_d = (1. - u) * left_v + u * right_v; + let s_b = top_left * (1. - u) * (1. - v) + top_right * u * (1. - v) + bottom_left * (1. - u) * v + bottom_right * u * v; + + Some(MeshSubpatchVertex { + position: mesh_transform.transform_point2(s_c + s_d - s_b), + gamma_color: self.eval_color(u as f32, v as f32)?, + }) + } +} + +/// Struct for evaluating color for subpatch corners. +/// The main purpose is to prevent duplicated calculation of the slopes for hermite interpolation for each subpatch. +#[derive(Clone)] +pub struct MeshGradientEvaluator { + /// List of required data for color interpolation, row major order. + patches: Vec, +} + +impl MeshGradientEvaluator { + // TODO: probably it is better to use u/v for slope calculation + pub fn new(mesh_gradient: &MeshGradient) -> Option { + let &MeshGradient { corner_columns, corner_rows, .. } = mesh_gradient; + if corner_rows < 2 || corner_columns < 2 { + return None; + } + let patch_columns = corner_columns - 1; + let patch_rows = corner_rows - 1; + + if mesh_gradient.patch_defs.len() != patch_rows * patch_columns { + return None; + } + let corner_count = corner_rows.checked_mul(corner_columns)?; + if mesh_gradient.corner_points.len() != corner_count || mesh_gradient.corner_colors.len() != corner_count { + return None; + } + + let corner_positions: Vec = mesh_gradient + .corner_points + .iter() + .map(|&point_id| mesh_gradient.mesh_geometry.point_domain.position_from_id(point_id)) + .collect::>()?; + + // We need to calculate the color derivatives in 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 gamma_colors: Vec = mesh_gradient.corner_colors.iter().map(|color| Vec4::from_array(color.to_gamma_srgb_channels())).collect(); + + // Calculate the slope of the `curr_index` corner by FDM. The slope is derived from the linear distance from the previous/next corners. + let calculate_color_slope = |prev_index: usize, curr_index: usize, next_index: usize| { + let prev_color = gamma_colors[prev_index]; + let curr_color = gamma_colors[curr_index]; + let next_color = gamma_colors[next_index]; + + let [prev_pos, curr_pos, next_pos] = [prev_index, curr_index, next_index].map(|index| corner_positions[index]); + let prev_distance = curr_pos.distance(prev_pos) as f32; + let next_distance = next_pos.distance(curr_pos) as f32; + + if prev_index == curr_index { + // FIXME: resolve zero-division problem + (next_color - curr_color) / next_distance + } else if next_index == curr_index { + (curr_color - prev_color) / prev_distance + } else { + let backward_diff = (curr_color - prev_color) / prev_distance; + let forward_diff = (next_color - curr_color) / next_distance; + let central_diff = (backward_diff + forward_diff) / 2.; + + // Prevent overshooting by applying a zero slope at local minimum/maximum + // TODO: consider clamping slope by a constant value + Vec4::from_array(std::array::from_fn( + |channel| { + if backward_diff[channel] * forward_diff[channel] <= 0. { 0. } else { central_diff[channel] } + }, + )) + } + }; + + let sample_index = |row: isize, column: isize| -> usize { + let clamped_column = column.clamp(0, corner_columns as isize - 1) as usize; + let clamped_row = row.clamp(0, corner_rows as isize - 1) as usize; + clamped_row * corner_columns + clamped_column + }; + + let mut corner_slopes = Vec::with_capacity(corner_rows * corner_columns); + for row in 0..corner_rows as isize { + for col in 0..corner_columns as isize { + let curr_index = sample_index(row, col); + let u = calculate_color_slope(sample_index(row, col - 1), curr_index, sample_index(row, col + 1)); + let v = calculate_color_slope(sample_index(row - 1, col), curr_index, sample_index(row + 1, col)); + corner_slopes.push(MeshCornerDerivatives { u, v }); + } + } + + let mut patch_color_data: Vec = vec![]; + for patch_def in &mesh_gradient.patch_defs { + let patch = mesh_gradient.resolve_patch(patch_def)?; + let gamma_colors = patch_def + .corner_indices + .map(|index| gamma_colors.get(index).copied()) + .into_iter() + .collect::>>()? + .try_into() + .ok()?; + + let color_slopes = patch_def + .corner_indices + .map(|index| corner_slopes.get(index).copied()) + .into_iter() + .collect::>>()? + .try_into() + .ok()?; + + let [top_left_pos, top_right_pos, bottom_left_pos, bottom_right_pos] = patch.corners; + let lengths = [ + top_left_pos.distance(top_right_pos) as f32, // top + bottom_left_pos.distance(bottom_right_pos) as f32, // bottom + top_left_pos.distance(bottom_left_pos) as f32, // left + top_right_pos.distance(bottom_right_pos) as f32, // right + ]; + patch_color_data.push(MeshPatchEvaluator { + corners: patch.corners, + edges: patch.edges, + gamma_colors, + color_slopes, + lengths, + }); + } + + Some(Self { patches: patch_color_data }) + } + + /// Subdivide all patches in a mesh into parallelogram subpatches so to renderable by two linear gradients with mask. + /// Returns subpatchs in row-major. + pub fn subdivide_patches(&self, subdivisions_per_patch_per_axis: usize, mesh_transform: DAffine2) -> Option> { + let count = subdivisions_per_patch_per_axis; + if count == 0 { + return None; + } + + let capacity = self.patches.len().checked_mul(count)?.checked_mul(count)?; + let mut subpatches = Vec::with_capacity(capacity); + + for patch in &self.patches { + let evaluate_row = |row: usize| -> Option> { + let v = row as f64 / count as f64; + + (0..=count) + .map(|column| { + let u = column as f64 / count as f64; + patch.eval_vertex(u, v, mesh_transform) + }) + .collect() + }; + + // Reusing the previous bottom row as a current top row to prevent duplicated evaluation on the same subpatch vertices. + let mut top_row = evaluate_row(0)?; + for row in 0..count { + let bottom_row = evaluate_row(row + 1)?; + for column in 0..count { + subpatches.push(MeshSubpatch { + corners: [top_row[column], top_row[column + 1], bottom_row[column], bottom_row[column + 1]], + }); + } + + top_row = bottom_row; + } + } + + Some(subpatches) + } +} + +impl RenderComplexity for MeshGradient { + fn render_complexity(&self) -> usize { + let patch_rows = self.corner_rows.saturating_sub(1); + let patch_columns = self.corner_columns.saturating_sub(1); + + // FIXME: probably better to calculate the approximate number of subpatchs + let subpatch_count_per_patch = 64; + patch_rows + .saturating_mul(patch_columns) + .saturating_mul(subpatch_count_per_patch) + .saturating_mul(subpatch_count_per_patch) + } +} + /// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both /// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint /// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks. @@ -576,3 +968,17 @@ impl core_types::bounds::BoundingBox for Gradient { core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)]) } } + +impl core_types::bounds::BoundingBox for MeshGradient { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox { + // FIXME: infinite? finite? + core_types::bounds::RenderBoundingBox::Infinite + } + + fn thumbnail_bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox { + // FIXME: implement actual check of the bounding box + let start = transform.transform_point2(DVec2::ZERO); + let end = transform.transform_point2(DVec2::X); + core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)]) + } +} diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index 63f9b87650..04686239b0 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -941,6 +941,15 @@ impl Vector { self.segment_points_from_id(id).map(|(_, _, bezier)| bezier) } + /// Tries to convert a segment with the specified id to a [`PathSeg`], returning None if the id is invalid. + pub fn path_segment_from_id(&self, id: SegmentId) -> Option { + let segment_index = self.segment_domain.id_to_index(id)?; + let start_index = *self.segment_domain.start_point().get(segment_index)?; + let end_index = *self.segment_domain.end_point().get(segment_index)?; + let handles = *self.segment_domain.handles().get(segment_index)?; + Some(self.path_segment_from_index(start_index, end_index, handles)) + } + /// Tries to convert a segment with the specified id to the start and end points and a [`Bezier`], returning None if the id is invalid. pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, Bezier)> { Some(self.segment_points_from_index(self.segment_domain.id_to_index(id)?)) diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index bcfed58a5a..de0dfac609 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -297,6 +297,8 @@ fn flatten_vector(graphic_list: &List) -> List { Item::from_parts(element, attributes) }) .collect::>(), + // FIXME: + Graphic::MeshGradient(mesh_gradient) => Vec::new(), Graphic::Text(text) => { // Shape the glyphs into vectors (each item's own transform is applied), then compose the parent's transform like the other arms let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); From e6169a9a64c9498da077ba8b8041014c44000d6d Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Thu, 16 Jul 2026 11:57:52 +0900 Subject: [PATCH 6/8] wip: adding editor integration --- node-graph/graph-craft/src/document/value.rs | 10 +- node-graph/graph-craft/src/proto.rs | 2 +- .../libraries/vector-types/src/gradient.rs | 123 +++++++++++++++++- 3 files changed, 126 insertions(+), 9 deletions(-) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index cd28de5d4b..33e7dc5129 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -15,7 +15,7 @@ use graphene_application_io::resource::ResourceId; use graphic_types::raster_types::{CPU, Image, Raster}; use graphic_types::vector_types::vector::misc::BoxCorners; use graphic_types::vector_types::vector::style::DashPattern; -use graphic_types::vector_types::vector::style::Gradient; +use graphic_types::vector_types::vector::style::{Gradient, MeshGradient}; use graphic_types::vector_types::vector::{self, ReferencePoint}; use graphic_types::{Artboard, Graphic, Vector}; use rendering::RenderMetadata; @@ -96,6 +96,8 @@ macro_rules! tagged_value { #[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_to_gradient")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")] Gradient(Gradient), + /// Stored compactly as a `MeshGradient`, materializing as an `Item` at runtime. + MeshGradient(MeshGradient), /// Stored compactly as a `Vec`, materializes as the single-value `Item` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. #[serde(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "BrushStrokeTable")] @@ -141,6 +143,7 @@ macro_rules! tagged_value { Self::F64Array(values) => values.cache_hash(state), Self::Color(color) => color.cache_hash(state), Self::Gradient(stops) => stops.cache_hash(state), + Self::MeshGradient(mesh_gradient) => mesh_gradient.cache_hash(state), Self::BrushStrokes(strokes) => strokes.cache_hash(state), // ======================= // NON-SERIALIZED VARIANTS @@ -203,6 +206,7 @@ macro_rules! tagged_value { } Self::Color(color) => Box::new(Item::new_from_element(color)), Self::Gradient(stops) => Box::new(Item::new_from_element(stops)), + Self::MeshGradient(mesh_gradient) => Box::new(Item::new_from_element(mesh_gradient)), Self::BrushStrokes(strokes) => Box::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))), // ======================= // AUTO-GENERATED VARIANTS @@ -265,6 +269,7 @@ macro_rules! tagged_value { } Self::Color(color) => Arc::new(Item::new_from_element(color)), Self::Gradient(stops) => Arc::new(Item::new_from_element(stops)), + Self::MeshGradient(mesh_gradient) => Arc::new(Item::new_from_element(mesh_gradient)), Self::BrushStrokes(strokes) => Arc::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))), // ======================= // AUTO-GENERATED VARIANTS @@ -293,6 +298,7 @@ macro_rules! tagged_value { Self::F64Array(_) => list!(f64), Self::Color(_) => item!(Color), Self::Gradient(_) => item!(Gradient), + Self::MeshGradient(_) => item!(MeshGradient), Self::BrushStrokes(_) => item!(BrushTrace), // ======================= // AUTO-GENERATED VARIANTS @@ -372,6 +378,7 @@ macro_rules! tagged_value { if name == std::any::type_name::<()>() { return Some(TaggedValue::None) } if name == std::any::type_name::() { return Some(TaggedValue::Color(Color::default())) } if name == std::any::type_name::() { return Some(TaggedValue::Gradient(Gradient::default())) } + if name == std::any::type_name::() { return Some(TaggedValue::MeshGradient(MeshGradient::default())) } $( if name == std::any::type_name::<$ty>() { return Some(TaggedValue::$identifier(Default::default())) } )* if name == std::any::type_name::() { return Some(TaggedValue::BrushStrokes(Vec::new())) } // Unranked types without a variant route through `TypeDefault`, with `to_dynany`/`to_any` constructing the actual default at execution time @@ -425,6 +432,7 @@ macro_rules! tagged_value { Self::F64Array(values) => format!("F64Array({values:?})"), Self::Color(color) => format!("Color({color:?})"), Self::Gradient(stops) => format!("Gradient({stops:?})"), + Self::MeshGradient(mesh_gradient) => format!("MeshGradient({mesh_gradient:?})"), Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"), // ======================= // AUTO-GENERATED VARIANTS diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index b8f95a434f..85970e91b0 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -1059,7 +1059,7 @@ mod test { // If this assert fails: These NodeIds seem to be changing when you modify TaggedValue, just update them. assert_eq!( ids, - vec![NodeId(12815475172301479638), NodeId(13251389748338817266), NodeId(7166921994790432021), NodeId(15318519137317483318)] + vec![NodeId(8464972237805743576), NodeId(3528778906331798968), NodeId(1126597937993520391), NodeId(17582929706900579130)] ); } diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 7ca2a6135f..b0e8078c1e 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -7,7 +7,7 @@ use kurbo::{ParamCurve, PathSeg}; use crate::{ Vector, - vector::{PointId, SegmentId, misc::point_to_dvec2}, + vector::{PointId, SegmentId, StrokeId, misc::point_to_dvec2}, }; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] @@ -511,15 +511,17 @@ pub struct MeshPatch { /// Definition of a mesh gradient patch. The entity is stored in the corresponding `MeshGradient` struct. #[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] struct MeshPatchDefinition { + /// Corner indices in MeshGradient::corner_points/corner_colors. [top-left, top-right, bottom-left, bottom-right] corner_indices: [usize; 4], + /// Segment ids of edges defining the patch. [top, bottom, left, right] edges: [SegmentId; 4], } /// Mesh gradient defined by multiple coons patches. #[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MeshGradient { mesh_geometry: Vector, pub corner_rows: usize, @@ -532,7 +534,113 @@ pub struct MeshGradient { patch_defs: Vec, } +impl Default for MeshGradient { + fn default() -> Self { + // Build 2x2 patches + let corner_rows = 3; + let corner_columns = 3; + let positions: Vec = (0..corner_rows) + .flat_map(|row| { + let v = row as f64 / (corner_rows - 1) as f64; + (0..corner_columns).map(move |column| { + let u = column as f64 / (corner_columns - 1) as f64; + DVec2::new(u, v) + }) + }) + .collect(); + + MeshGradient::from_positions(positions.as_slice(), corner_rows, corner_columns).expect("2x2 patches should be valid mesh gradient") + } +} + impl MeshGradient { + pub fn from_positions(positions: &[DVec2], corner_rows: usize, corner_columns: usize) -> Option { + if corner_rows < 2 || corner_columns < 2 { + return None; + } + + let corner_count = corner_rows.checked_mul(corner_columns)?; + if positions.len() != corner_count { + return None; + } + + let mut vector = Vector::default(); + let mut corner_points = Vec::with_capacity(corner_count); + + for &position in positions { + let point_id = vector.point_domain.next_id(); + vector.point_domain.push(point_id, position); + corner_points.push(point_id); + } + + let mut horizontal_edges = Vec::with_capacity(corner_rows * (corner_columns - 1)); + for row in 0..corner_rows { + for column in 0..(corner_columns - 1) { + let start_index = row * corner_columns + column; + let end_index = start_index + 1; + + let segment_id = vector.segment_domain.next_id(); + vector.push(segment_id, corner_points[start_index], corner_points[end_index], (None, None), StrokeId::ZERO); + horizontal_edges.push(segment_id); + } + } + + let mut vertical_edges = Vec::with_capacity((corner_rows - 1) * corner_columns); + for row in 0..(corner_rows - 1) { + for column in 0..corner_columns { + let start_index = row * corner_columns + column; + let end_index = start_index + corner_columns; + + let segment_id = vector.segment_domain.next_id(); + vector.push(segment_id, corner_points[start_index], corner_points[end_index], (None, None), StrokeId::ZERO); + vertical_edges.push(segment_id); + } + } + + // FIXME: alternative color is only for testing purpose + let corner_colors = (0..corner_rows) + .flat_map(|row| { + (0..corner_columns).map(move |column| { + let luminance = (row + column).is_multiple_of(2) as u8 as f32; + Color::from_luminance(luminance) + }) + }) + .collect(); + + let patch_rows = corner_rows - 1; + let patch_columns = corner_columns - 1; + let mut patch_defs = Vec::with_capacity((corner_rows - 1) * (corner_columns - 1)); + for row in 0..patch_rows { + for column in 0..patch_columns { + let top_edge_index = row * patch_columns + column; + let bottom_edge_index = (row + 1) * patch_columns + column; + let left_edge_index = row * corner_columns + column; + let right_edge_index = left_edge_index + 1; + let edges = [ + horizontal_edges[top_edge_index], + horizontal_edges[bottom_edge_index], + vertical_edges[left_edge_index], + vertical_edges[right_edge_index], + ]; + let top_left = row * corner_columns + column; + let top_right = top_left + 1; + let bottom_left = top_left + corner_columns; + let bottom_right = bottom_left + 1; + let corner_indices = [top_left, top_right, bottom_left, bottom_right]; + patch_defs.push(MeshPatchDefinition { corner_indices, edges }); + } + } + + Some(Self { + mesh_geometry: vector, + corner_rows, + corner_columns, + corner_points, + corner_colors, + patch_defs, + }) + } + fn resolve_patch(&self, patch_def: &MeshPatchDefinition) -> Option { // Normalizes the orientation of the patch edge. let resolve_oriented_edge = |segment_id: SegmentId, expected_start: PointId, expected_end: PointId| -> Option { @@ -811,11 +919,12 @@ impl MeshGradientEvaluator { .ok()?; let [top_left_pos, top_right_pos, bottom_left_pos, bottom_right_pos] = patch.corners; + // [top, bottom, left, right] let lengths = [ - top_left_pos.distance(top_right_pos) as f32, // top - bottom_left_pos.distance(bottom_right_pos) as f32, // bottom - top_left_pos.distance(bottom_left_pos) as f32, // left - top_right_pos.distance(bottom_right_pos) as f32, // right + top_left_pos.distance(top_right_pos) as f32, + bottom_left_pos.distance(bottom_right_pos) as f32, + top_left_pos.distance(bottom_left_pos) as f32, + top_right_pos.distance(bottom_right_pos) as f32, ]; patch_color_data.push(MeshPatchEvaluator { corners: patch.corners, From 8bae0fb7d87268f1149f29b4fa069adca11f49a3 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Fri, 17 Jul 2026 13:29:56 +0900 Subject: [PATCH 7/8] Add "Mesh Gradient Value" node --- .../interpreted-executor/src/node_registry.rs | 11 +- node-graph/libraries/vector-types/src/lib.rs | 2 +- node-graph/nodes/gstd/src/lib.rs | 2 +- node-graph/nodes/math/src/lib.rs | 8 +- node-graph/nodes/vector/src/vector_nodes.rs | 115 +++++++++++------- 5 files changed, 90 insertions(+), 48 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index c7f0bc5c80..be0088e97c 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -7,7 +7,7 @@ use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graphene_std::any::DynAnyNode; use graphene_std::brush::brush_stroke::BrushTrace; use graphene_std::extract_xy::XY; -use graphene_std::gradient::Gradient; +use graphene_std::gradient::{Gradient, MeshGradient}; use graphene_std::list::{AttributeValueDyn, Bundle, Item, List, ListDyn, NodeIdPath}; #[cfg(target_family = "wasm")] use graphene_std::platform_application_io::canvas_utils::CanvasHandle; @@ -40,6 +40,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => List>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), @@ -48,6 +49,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Item>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item]), @@ -97,6 +99,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => List>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), @@ -114,6 +117,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Item>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), @@ -320,6 +324,7 @@ fn node_registry() -> HashMap HashMap, Color, Gradient, + MeshGradient, f64, bool, String, @@ -417,6 +423,7 @@ fn node_registry() -> HashMap HashMap), attribute_value_node!(List), attribute_value_node!(List), + attribute_value_node!(List), attribute_value_node!(List), attribute_value_node!(List>), #[cfg(feature = "gpu")] @@ -571,6 +579,7 @@ fn node_registry() -> HashMap), transform_list_node!(element: Color), transform_list_node!(element: Gradient), + transform_list_node!(element: MeshGradient), ]; node_types.extend(transform_list_rows); let mut map: HashMap> = HashMap::new(); diff --git a/node-graph/libraries/vector-types/src/lib.rs b/node-graph/libraries/vector-types/src/lib.rs index d66703a690..781d9fe228 100644 --- a/node-graph/libraries/vector-types/src/lib.rs +++ b/node-graph/libraries/vector-types/src/lib.rs @@ -8,7 +8,7 @@ pub mod vector; // Re-export commonly used types at the crate root pub use core_types as gcore; -pub use gradient::{Gradient, GradientSpreadMethod, GradientStop, GradientType}; +pub use gradient::{Gradient, GradientSpreadMethod, GradientStop, GradientType, MeshGradient}; pub use math::{QuadExt, RectExt}; pub use subpath::Subpath; pub use vector::Vector; diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index 54aa4084e1..525967b286 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -58,7 +58,7 @@ pub mod subpath { } pub mod gradient { - pub use vector_types::{Gradient, GradientStop}; + pub use vector_types::{Gradient, GradientStop, MeshGradient}; } pub mod transform { diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 1474aa2be3..a62d7f9ba4 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -14,7 +14,7 @@ use math_parser::value::{Number, Value}; use num_traits::Pow; use rand::{Rng, SeedableRng}; use std::ops::{Add, Div, Mul, Rem, Sub}; -use vector_types::Gradient; +use vector_types::{Gradient, MeshGradient}; /// The struct that stores the context for the maths parser. /// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs. @@ -1016,6 +1016,12 @@ fn spread_method(_: impl Ctx, gradient: Item, spread_method: Item) -> Item { + mesh_gradient +} + /// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). #[node_macro::node(category("Color"))] fn sample_gradient(_: impl Ctx, _primary: (), gradient: Item, position: Item) -> Item { diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index c2efc76a16..c1f4b39780 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -33,7 +33,7 @@ use vector_types::vector::misc::{ }; use vector_types::vector::style::{DashPattern, Gradient, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; -use vector_types::{GradientSpreadMethod, GradientType}; +use vector_types::{GradientSpreadMethod, GradientType, MeshGradient}; /// Implemented for `List` types that contain vector items reachable via mutable access. /// Used by the whole-collection Assign Colors node so it can apply to either `List` or `List`. @@ -178,13 +178,13 @@ where async fn fill( _: impl Ctx, /// The content with vector paths to apply the fill style to. - #[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)] + #[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)] content: Item, /// The fill to paint the path with. #[default(Color::BLACK)] #[implementations( - List, List, List, List, List>, List>, - List, List, List, List, List>, List>, + List, List, List, List, List, List>, List>, + List, List, List, List, List, List>, List>, )] fill: F, _backup_color: Item, @@ -203,51 +203,78 @@ where let mut content = content; let mut fill = fill.into_graphic_list(); - // Stamp the gradient styling inputs onto any gradient paint missing them, whether the paint arrived as a picker value or a wire - for graphic in fill.iter_element_values_mut() { - let Graphic::Gradient(gradient) = graphic else { continue }; - - if gradient.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_none() { - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_GRADIENT_TYPE) { - *value = _gradient_type; - } - } - - if gradient.iter_attribute_values::(ATTR_SPREAD_METHOD).is_none() { - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_SPREAD_METHOD) { - *value = _spread_method; - } - } - - if gradient.iter_attribute_values::(ATTR_TRANSFORM).is_none() { - // Without an explicit placement, derive one covering the paint target's bounding box (the CSS `auto` behavior) - let transform = if _has_transform { - _transform - } else { - let mut bounds: Option<[DVec2; 2]> = None; - content.for_each_vector_mut(|vector, _| { - if let Some([min, max]) = vector.bounding_box() { - bounds = Some(match bounds { - Some([bmin, bmax]) => [bmin.min(min), bmax.max(max)], - None => [min, max], - }); - } + let mut vector_bounds = || { + let mut bounds: Option<[DVec2; 2]> = None; + content.for_each_vector_mut(|vector, _| { + if let Some([min, max]) = vector.bounding_box() { + bounds = Some(match bounds { + Some([bmin, bmax]) => [bmin.min(min), bmax.max(max)], + None => [min, max], }); - - // Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box` - let [min, mut max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]); + } + }); + bounds + .map(|bounds| { + let [min, mut max] = bounds; if max.x - min.x < 1e-10 { max.x = min.x + 1.; } if max.y - min.y < 1e-10 { max.y = min.y + 1.; } - initial_gradient_transform_for_bounding_box([min, max]) - }; + [min, max] + }) + .unwrap_or([DVec2::ZERO, DVec2::ONE]) + }; + + // Stamp the gradient styling inputs onto any gradient paint missing them, whether the paint arrived as a picker value or a wire + for graphic in fill.iter_element_values_mut() { + match graphic { + Graphic::Gradient(gradient) => { + if gradient.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_none() { + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_GRADIENT_TYPE) { + *value = _gradient_type; + } + } + + if gradient.iter_attribute_values::(ATTR_SPREAD_METHOD).is_none() { + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_SPREAD_METHOD) { + *value = _spread_method; + } + } + + if gradient.iter_attribute_values::(ATTR_TRANSFORM).is_none() { + // Without an explicit placement, derive one covering the paint target's bounding box (the CSS `auto` behavior) + let transform = if _has_transform { + _transform + } else { + // Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box` + let [min, max] = vector_bounds(); + initial_gradient_transform_for_bounding_box([min, max]) + }; - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { - *value = transform; + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + *value = transform; + } + } + } + Graphic::MeshGradient(mesh_gradient) => { + if mesh_gradient.iter_attribute_values::(ATTR_TRANSFORM).is_none() { + // Without an explicit placement, derive one covering the paint target's bounding box (the CSS `auto` behavior) + let transform = if _has_transform { + _transform + } else { + let [min, max] = vector_bounds(); + let size = max - min; + DAffine2::from_cols(DVec2::new(size.x, 0.), DVec2::new(0., size.y), min) + }; + + for value in mesh_gradient.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + *value = transform; + } + } } + _ => continue, } } @@ -260,13 +287,13 @@ where async fn stroke( _: impl Ctx, /// The content with vector paths to apply the stroke style to. - #[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)] + #[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)] content: Item, /// The stroke paint. #[default(Color::BLACK)] #[implementations( - List, List, List, List, List>, List>, - List, List, List, List, List>, List>, + List, List, List, List, List, List>, List>, + List, List, List, List, List, List>, List>, )] paint: P, /// The stroke thickness. @@ -324,7 +351,7 @@ where vector.stroke = Some(stroke); }); - let paint = paint.into_graphic_list(); + let paint: List = paint.into_graphic_list(); content.set_vector_paint(ATTR_STROKE, paint); content } From 231773c26820bab8a6126e5b7a69d5d4cf5c4571 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Fri, 17 Jul 2026 13:30:27 +0900 Subject: [PATCH 8/8] Fix antialiasing artifact between subpatches --- .../libraries/rendering/src/renderer.rs | 139 +++++++++--------- 1 file changed, 68 insertions(+), 71 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index d76560c13d..97837e68b6 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2288,85 +2288,82 @@ impl Render for List { continue; }; + let clip_inflation = 0.01; + let paint_inflation = 0.02; + let clip_min = -clip_inflation; + let clip_size = 1. + 2. * clip_inflation; + let paint_min = -paint_inflation; + let paint_size = 1. + 2. * paint_inflation; + let shared_id = generate_uuid(); + write!( + &mut render.svg_defs, + r##" + + + "##, + ) + .unwrap(); + let mut unique_id = generate_uuid(); subpatches.iter().for_each(|subpatch| { let [top_left, top_right, bottom_left, bottom_right] = subpatch.corners; let subpatch_transform = format_transform_matrix(DAffine2::from_cols(top_right.position - top_left.position, bottom_left.position - top_left.position, top_left.position)); // linear gradient for the bottom line - write!( - &mut render.svg_defs, - r##""##, - float_gamma_color_to_hex(bottom_left.gamma_color), - float_gamma_color_to_hex(bottom_right.gamma_color), - ) - .unwrap(); - - // linear gradient for the top line - write!( - &mut render.svg_defs, - r##""##, - float_gamma_color_to_hex(top_left.gamma_color), - float_gamma_color_to_hex(top_right.gamma_color), - ) - .unwrap(); - - // top mask gradient - write!( - &mut render.svg_defs, - r##""##, - ) - .unwrap(); - - // Inflate the subpatch to hide gaps caused by anti-aliasing - let inflation_amount = 1.; - - let top_normal = (top_left.position - top_right.position).perp().normalize(); - let bottom_normal = (bottom_right.position - bottom_left.position).perp().normalize(); - let left_normal = (bottom_left.position - top_left.position).perp().normalize(); - let right_normal = (top_right.position - bottom_right.position).perp().normalize(); - - let inflated_top_left_pos = top_left.position + (top_normal + left_normal) * inflation_amount; - let inflated_top_right_pos = top_right.position + (top_normal + right_normal) * inflation_amount; - let inflated_bottom_left_pos = bottom_left.position + (bottom_normal + left_normal) * inflation_amount; - let inflated_bottom_right_pos = bottom_right.position + (bottom_normal + right_normal) * inflation_amount; - - let DVec2 { x: top_left_x, y: top_left_y } = inflated_top_left_pos; - let DVec2 { x: top_right_x, y: top_right_y } = inflated_top_right_pos; - let DVec2 { x: bottom_left_x, y: bottom_left_y } = inflated_bottom_left_pos; - let DVec2 { x: bottom_right_x, y: bottom_right_y } = inflated_bottom_right_pos; - - // mask - let min_x = bottom_left_x.min(top_left_x.min(bottom_right_x.min(top_right_x))); - let max_x = bottom_left_x.max(top_left_x.max(bottom_right_x.max(top_right_x))); - let min_y = bottom_left_y.min(top_left_y.min(bottom_right_y.min(top_right_y))); - let max_y = bottom_left_y.max(top_left_y.max(bottom_right_y.max(top_right_y))); - let mask_width = max_x - min_x; - let mask_height = max_y - min_y; - write!( - &mut render.svg_defs, - r##" - "##, - ) - .unwrap(); - - render.leaf_tag("polygon", |attributes| { - attributes.push("points", format!("{bottom_left_x},{bottom_left_y} {bottom_right_x},{bottom_right_y} {top_right_x},{top_right_y} {top_left_x},{top_left_y}")); - attributes.push("fill", format!("url(#gb{unique_id})")); - }); + write!( + &mut render.svg_defs, + r##""##, + float_gamma_color_to_hex(bottom_left.gamma_color), + float_gamma_color_to_hex(bottom_right.gamma_color), + ) + .unwrap(); + + // linear gradient for the top line + write!( + &mut render.svg_defs, + r##""##, + float_gamma_color_to_hex(top_left.gamma_color), + float_gamma_color_to_hex(top_right.gamma_color), + ) + .unwrap(); - render.leaf_tag("polygon", |attributes| { - attributes.push("points", format!("{bottom_left_x},{bottom_left_y} {bottom_right_x},{bottom_right_y} {top_right_x},{top_right_y} {top_left_x},{top_left_y}")); - attributes.push("fill", format!("url(#gt{unique_id})")); - attributes.push("mask", format!("url(#m{unique_id})")); - }); + render.parent_tag( + "g", + |attributes| { + attributes.push(ATTR_TRANSFORM, subpatch_transform); + attributes.push("clip-path", format!("url(#mc{shared_id})")); + }, + |render| { + // Force both gradient layers to composite before the outer clip applies edge coverage. + render.parent_tag( + "g", + |attributes| { + attributes.push("style", "isolation:isolate"); + attributes.push("mask", format!("url(#mi{shared_id})")); + }, + |render| { + render.leaf_tag("rect", |attributes| { + attributes.push("x", paint_min.to_string()); + attributes.push("y", paint_min.to_string()); + attributes.push("width", paint_size.to_string()); + attributes.push("height", paint_size.to_string()); + attributes.push("fill", format!("url(#gb{unique_id})")); + }); + + render.leaf_tag("rect", |attributes| { + attributes.push("x", paint_min.to_string()); + attributes.push("y", paint_min.to_string()); + attributes.push("width", paint_size.to_string()); + attributes.push("height", paint_size.to_string()); + attributes.push("fill", format!("url(#gt{unique_id})")); + attributes.push("mask", format!("url(#mm{shared_id})")); + }); + }, + ); + }, + ); - unique_id += 1; + unique_id += 1; }); } }