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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion node-graph/libraries/graphic-types/src/graphic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ use core_types::ops::TableConvert;
use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
use dyn_any::DynAny;
use glam::DAffine2;
use raster_types::{CPU, GPU, Raster};
use vector_types::GradientStops;
// use vector_types::Vector;

pub use vector_types::Vector;
use vector_types::vector::style::Fill;

/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
Expand Down Expand Up @@ -193,6 +194,24 @@ fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic
output
}

/// Converts a `Fill` enum into the `Table<Graphic>` representation used as paint storage.
/// TODO: Remove once all paint sources flow through `Table<Graphic>` directly without going through the `Fill` enum.
pub fn fill_to_paint(fill: &Fill) -> Option<Table<Graphic>> {
match fill {
Fill::None => None,
Fill::Solid(color) => Some(Table::new_from_element((*color).into())),
Fill::Gradient(gradient) => {
let gradient_row = TableRow::new_from_element(gradient.stops.clone())
.with_attribute(ATTR_TRANSFORM, gradient.to_transform())
.with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type)
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method);
let gradient_table = Table::new_from_row(gradient_row);

Some(Table::new_from_element(Graphic::Gradient(gradient_table)))
}
}
}

/// Maps from a concrete element type to its corresponding `Graphic` enum variant,
/// enabling type-directed casting of typed `Table`s from a `Graphic` value.
pub trait TryFromGraphic: Clone + Sized {
Expand Down
135 changes: 76 additions & 59 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use core_types::{
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphene_hash::CacheHashWrapper;
use graphic_types::graphic::fill_to_paint;
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::subpath::Subpath;
Expand Down Expand Up @@ -1186,70 +1187,86 @@ impl Render for Table<Vector> {
let use_layer = can_draw_aligned_stroke;
let wants_stroke_below = stroke.as_ref().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow);

// Closures to avoid duplicated fill/stroke drawing logic
let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match element.style.fill() {
Fill::Solid(color) => {
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
}
Fill::Gradient(gradient) => {
let mut stops = peniko::ColorStops::new();
for (position, color, _) in gradient.stops.interpolated_samples() {
stops.push(peniko::ColorStop {
offset: position as f32,
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
});
}

let bounds = element.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);

let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
parent_transform.inverse()
} else {
Default::default()
};
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
// TODO: This conversion is only necessary during the transition period from Fill to Table<Graphic>
let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| {
let Some(paint_table) = fill_to_paint(element.style.fill()) else {
return;
};

let start = mod_points.transform_point2(gradient.start);
let end = mod_points.transform_point2(gradient.end);
for paint_idx in 0..paint_table.len() {
let Some(paint) = paint_table.element(paint_idx) else { continue };
match paint {
Graphic::Color(table) => {
let Some(color) = table.element(0) else { continue };

let fill = peniko::Brush::Gradient(peniko::Gradient {
kind: match gradient.gradient_type {
GradientType::Linear => peniko::LinearGradientPosition {
start: to_point(start),
end: to_point(end),
}
.into(),
GradientType::Radial => {
let radius = start.distance(end);
peniko::RadialGradientPosition {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(start),
end_radius: radius as f32,
}
.into()
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
}
Graphic::Gradient(stops_table) => {
let Some(stops) = stops_table.element(0) else { continue };
let gradient_type: GradientType = stops_table.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
let gradient_transform: DAffine2 = stops_table.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let spread_method: GradientSpreadMethod = stops_table.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);

let mut peniko_stops = peniko::ColorStops::new();
for (position, color, _) in stops.interpolated_samples() {
peniko_stops.push(peniko::ColorStop {
offset: position as f32,
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
});
}
},
extend: match gradient.spread_method {
GradientSpreadMethod::Pad => peniko::Extend::Pad,
GradientSpreadMethod::Reflect => peniko::Extend::Reflect,
GradientSpreadMethod::Repeat => peniko::Extend::Repeat,
},
stops,
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
..Default::default()
});
let inverse_element_transform = if element_transform.matrix2.determinant() != 0. {
element_transform.inverse()
} else {
Default::default()

let bounds = element.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);

let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
parent_transform.inverse()
} else {
Default::default()
};
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform * gradient_transform;

let start = mod_points.transform_point2(DVec2::ZERO);
let end = mod_points.transform_point2(DVec2::X);

let fill = peniko::Brush::Gradient(peniko::Gradient {
kind: match gradient_type {
GradientType::Linear => peniko::LinearGradientPosition {
start: to_point(start),
end: to_point(end),
}
.into(),
GradientType::Radial => {
let radius = start.distance(end);
peniko::RadialGradientPosition {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(start),
end_radius: radius as f32,
}
.into()
}
},
extend: match spread_method {
GradientSpreadMethod::Pad => peniko::Extend::Pad,
GradientSpreadMethod::Reflect => peniko::Extend::Reflect,
GradientSpreadMethod::Repeat => peniko::Extend::Repeat,
},
stops: peniko_stops,
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
..Default::default()
});
let inverse_element_transform = if element_transform.matrix2.determinant() != 0. {
element_transform.inverse()
} else {
Default::default()
};
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), path);
}
_ => todo!(),
};
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), path);
}
Fill::None => {}
};

// Branching vectors without regions (e.g. mesh grids) need face-by-face fill rendering.
Expand Down
30 changes: 30 additions & 0 deletions node-graph/libraries/vector-types/src/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,12 @@ impl Gradient {

Some(index)
}

/// Builds the affine that places the gradient endpoints at `start` and `end` when applied to canonical gradient space (0,0) -> (1,0)
pub fn to_transform(&self) -> DAffine2 {
let direction = self.end - self.start;
DAffine2::from_cols(direction, direction.perp(), self.start)
}
}

// TODO: Eventually remove this migration document upgrade code
Expand Down Expand Up @@ -556,3 +562,27 @@ impl core_types::bounds::BoundingBox for GradientStops {
core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)])
}
}

#[cfg(test)]
mod tests {
use super::*;
use glam::DVec2;

fn linear_gradient(start: DVec2, end: DVec2) -> Gradient {
Gradient { start, end, ..Default::default() }
}

#[test]
fn to_transform_roundtrip() {
let cases = [(DVec2::ZERO, DVec2::X), (DVec2::new(10., 20.), DVec2::new(50., 30.)), (DVec2::new(-5., -5.), DVec2::new(5., 3.))];

for (start, end) in cases {
let transform = linear_gradient(start, end).to_transform();
let recovered_start = transform.transform_point2(DVec2::ZERO);
let recovered_end = transform.transform_point2(DVec2::X);

assert!((recovered_start - start).length() < 1e-10);
assert!((recovered_end - end).length() < 1e-10);
}
}
}
Loading