Skip to content
Merged
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
26 changes: 17 additions & 9 deletions node-graph/libraries/graphic-types/src/graphic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,13 +533,21 @@ impl Graphic {
}
Graphic::RasterCPU(_) | Graphic::RasterCPUList(_) => false,
Graphic::RasterGPU(_) | Graphic::RasterGPUList(_) => false,
Graphic::Color(item) => item.element().is_opaque(),
Graphic::ColorList(list) => list.element(0).is_some_and(|color| color.is_opaque()),
Graphic::Color(item) => item_opacity_is_full(item) && item.element().is_opaque(),
Graphic::ColorList(list) => !list.is_empty() && every_item_has_full_opacity(list) && list.iter_element_values().all(|color| color.is_opaque()),
// A `Clear` spread cuts off to transparency past the ends, leaving the rest of the region unpainted
Graphic::Gradient(item) => item.attribute_cloned_or_default::<GradientSpread>(ATTR_GRADIENT_SPREAD) != GradientSpread::Clear && item.element().iter().all(|stop| stop.color.is_opaque()),
Graphic::Gradient(item) => {
item_opacity_is_full(item)
&& item.attribute_cloned_or_default::<GradientSpread>(ATTR_GRADIENT_SPREAD) != GradientSpread::Clear
&& item.element().iter().all(|stop| stop.color.is_opaque())
}
Graphic::GradientList(list) => {
list.attribute_cloned_or_default::<GradientSpread>(ATTR_GRADIENT_SPREAD, 0) != GradientSpread::Clear
&& list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.is_opaque()))
!list.is_empty()
&& every_item_has_full_opacity(list)
&& (0..list.len()).all(|index| {
list.attribute_cloned_or_default::<GradientSpread>(ATTR_GRADIENT_SPREAD, index) != GradientSpread::Clear
&& list.element(index).is_some_and(|stops| stops.iter().all(|stop| stop.color.is_opaque()))
})
}
Graphic::Text(_) | Graphic::TextList(_) => false,
}
Expand All @@ -562,11 +570,11 @@ impl Graphic {
list.attribute::<Appearance>(ATTR_APPEARANCE, index),
)
}),
Graphic::Color(item) => item.element().a() == 0.,
Graphic::ColorList(list) => list.iter_element_values().all(|color| color.a() == 0.),
Graphic::Color(item) => item_opacity_is_zero(item) || item.element().a() == 0.,
Graphic::ColorList(list) => every_item_has_zero_opacity(list) || list.iter_element_values().all(|color| color.a() == 0.),
// A stopless ramp paints as solid black, matching `Gradient::evaluate`, so it counts as transparent only once it has stops
Graphic::Gradient(item) => !item.element().is_empty() && item.element().iter().all(|stop| stop.color.a() == 0.),
Graphic::GradientList(list) => list.iter_element_values().all(|stops| !stops.is_empty() && stops.iter().all(|stop| stop.color.a() == 0.)),
Graphic::Gradient(item) => item_opacity_is_zero(item) || (!item.element().is_empty() && item.element().iter().all(|stop| stop.color.a() == 0.)),
Graphic::GradientList(list) => every_item_has_zero_opacity(list) || list.iter_element_values().all(|stops| !stops.is_empty() && stops.iter().all(|stop| stop.color.a() == 0.)),
// Their content is never inspected, so zeroed opacity is the only invisibility these can report
Graphic::RasterCPU(item) => item_opacity_is_zero(item),
Graphic::RasterGPU(item) => item_opacity_is_zero(item),
Expand Down
54 changes: 38 additions & 16 deletions node-graph/libraries/rendering/src/render_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,19 @@ pub trait RenderExt {
) -> Self::Output;
}

/// The paint attribute for a solid color, or `none` when the color is absent.
fn render_color_paint(color: Option<&Color>, target: PaintTarget) -> String {
let Some(color) = color else {
return format!(r#" {}="none""#, target.paint_attr());
};
/// The paint attribute for a solid color, or the SVG `none` keyword when the color is absent.
/// `for_mask` keeps the fill opacity at full, as [`ItemRef::paint_opacity`] explains.
fn render_color_paint(item: Option<ItemRef<'_, Color>>, target: PaintTarget, for_mask: bool) -> String {
let unpainted = || format!(r#" {}="none""#, target.paint_attr());

let Some(item) = item else { return unpainted() };
let Some(color) = item.element() else { return unpainted() };

let alpha = color.a() * item.paint_opacity(for_mask);

let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(*color).to_rgb_hex());
if color.a() < 1. {
let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.);
if alpha < 1. {
let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (alpha * 1000.).round() / 1000.);
}

result
Expand All @@ -74,15 +78,16 @@ impl RenderExt for List<Color> {
_element_transform: DAffine2,
_stroke_transform: DAffine2,
_bounds: DAffine2,
_render_params: &RenderParams,
render_params: &RenderParams,
target: PaintTarget,
) -> Self::Output {
render_color_paint(self.element(0), target)
render_color_paint((!self.is_empty()).then_some(ItemRef::ListItem(self, 0)), target, render_params.for_mask)
}
}

/// Adds one gradient item's def into `svg_defs` and returns the gradient ID, or `None` when the item is absent.
fn render_gradient_paint(item: Option<ItemRef<'_, Gradient>>, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2) -> Option<u64> {
/// `for_mask` keeps the fill opacity at full, as [`ItemRef::paint_opacity`] explains.
fn render_gradient_paint(item: Option<ItemRef<'_, Gradient>>, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2, for_mask: bool) -> Option<u64> {
let mut stop = String::new();

let item = item?;
Expand All @@ -91,7 +96,14 @@ fn render_gradient_paint(item: Option<ItemRef<'_, Gradient>>, svg_defs: &mut Str
let local_gradient_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
let settings = gradient_settings_from_item(item);

let (samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
let (mut samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);

let paint_opacity = item.paint_opacity(for_mask);
if paint_opacity < 1. {
for (_, color, _) in &mut samples {
*color = color.with_alpha(color.a() * paint_opacity);
}
}

for (position, color, original_midpoint) in samples {
stop.push_str("<stop");
Expand All @@ -110,7 +122,11 @@ fn render_gradient_paint(item: Option<ItemRef<'_, Gradient>>, svg_defs: &mut Str

// A gradient with no stops paints as solid black, matching `Gradient::evaluate` (a stopless def would otherwise render as no paint per the SVG spec)
if stop.is_empty() {
stop.push_str(r##"<stop stop-color="#000000" />"##);
stop.push_str(r##"<stop stop-color="#000000""##);
if paint_opacity < 1. {
let _ = write!(stop, r#" stop-opacity="{}""#, (paint_opacity * 1000.).round() / 1000.);
}
stop.push_str(" />");
}

// Need to cancel out the element's transform as it is already applied to the path itself.
Expand Down Expand Up @@ -169,10 +185,16 @@ impl RenderExt for List<Gradient> {
element_transform: DAffine2,
_stroke_transform: DAffine2,
_bounds: DAffine2,
_render_params: &RenderParams,
render_params: &RenderParams,
_target: PaintTarget,
) -> Self::Output {
render_gradient_paint((!self.is_empty()).then_some(ItemRef::ListItem(self, 0)), svg_defs, item_transform, element_transform)
render_gradient_paint(
(!self.is_empty()).then_some(ItemRef::ListItem(self, 0)),
svg_defs,
item_transform,
element_transform,
render_params.for_mask,
)
}
}

Expand Down Expand Up @@ -253,9 +275,9 @@ impl RenderExt for List<Graphic> {
let paint_attr = target.paint_attr();

match fill_graphic {
Some(Graphic::Color(item)) => render_color_paint(Some(item.element()), target),
Some(Graphic::Color(item)) => render_color_paint(Some(ItemRef::Item(item)), target, render_params.for_mask),
Some(Graphic::ColorList(color_list)) => color_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target),
Some(Graphic::Gradient(item)) => render_gradient_paint(Some(ItemRef::Item(item)), svg_defs, item_transform, element_transform)
Some(Graphic::Gradient(item)) => render_gradient_paint(Some(ItemRef::Item(item)), svg_defs, item_transform, element_transform, render_params.for_mask)
.map(|gradient_id| format!(r##" {paint_attr}="url(#{gradient_id})""##))
.unwrap_or_else(|| format!(r#" {paint_attr}="none""#)),
Some(Graphic::GradientList(gradient_list)) => gradient_list
Expand Down
58 changes: 40 additions & 18 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ impl<'a, T> ItemRef<'a, T> {
}
}

/// The alpha multiplier this item's opacity attributes apply when it serves as a paint.
/// Fill opacity fades a paint just as opacity does, but a masker drops it so it cannot reach the content clipped to it.
pub(crate) fn paint_opacity(self, for_mask: bool) -> f32 {
let opacity_fill = if for_mask { 1. } else { self.attribute_cloned_or::<f64>(ATTR_OPACITY_FILL, 1.) };

(self.attribute_cloned_or::<f64>(ATTR_OPACITY, 1.) * opacity_fill) as f32
}

pub(crate) fn clone_item_attributes(self) -> core_types::list::ItemAttributeValues {
match self {
ItemRef::ListItem(list, index) => list.clone_item_attributes(index),
Expand Down Expand Up @@ -577,14 +585,28 @@ fn peniko_extend(gradient_spread: GradientSpread) -> peniko::Extend {
}
}

fn create_peniko_gradient_brush(gradient_item: ItemRef<'_, Gradient>, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
/// The Vello brush for one gradient item, paired with its placement transform.
/// `for_mask` keeps the fill opacity at full, as [`ItemRef::paint_opacity`] explains.
fn create_peniko_gradient_brush(gradient_item: ItemRef<'_, Gradient>, multiplied_transform: &DAffine2, for_mask: bool) -> Option<(peniko::Brush, DAffine2)> {
let stops = gradient_item.element()?;

let gradient_form: GradientForm = gradient_item.attribute_cloned_or_default(ATTR_GRADIENT_FORM);
let gradient_transform: DAffine2 = gradient_item.attribute_cloned_or_default(ATTR_TRANSFORM);
let settings = gradient_settings_from_item(gradient_item);

let (samples, span) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::VelloRampTexels);
let (mut samples, span) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::VelloRampTexels);

let paint_opacity = gradient_item.paint_opacity(for_mask);
if paint_opacity < 1. {
// A stopless ramp gets its black stop downstream, too late to be faded, so it needs one here instead
if samples.is_empty() {
samples.push((0., Color::BLACK, None));
}

for (_, color, _) in &mut samples {
*color = color.with_alpha(color.a() * paint_opacity);
}
}

let peniko_stops = peniko_color_stops(&samples);

Expand Down Expand Up @@ -1740,12 +1762,15 @@ fn render_vector_item_to_vello(

for paint_index in 0..fill_graphic.len() {
let Some(paint) = fill_graphic.element(paint_index) else { continue };
let solid_fill = |scene: &mut Scene, color: &Color| {
let fill = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color());
let solid_fill = |scene: &mut Scene, item: ItemRef<'_, Color>| {
let Some(color) = item.element() else { return };
let color = color.with_alpha(color.a() * item.paint_opacity(render_params.for_mask));

let fill = peniko::Brush::Solid(SRGBA8::from(color).to_peniko_color());
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
};
let gradient_fill = |scene: &mut Scene, gradient_item: ItemRef<'_, Gradient>| {
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(gradient_item, &multiplied_transform) else {
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(gradient_item, &multiplied_transform, render_params.for_mask) else {
return;
};

Expand All @@ -1760,11 +1785,8 @@ fn render_vector_item_to_vello(

match paint {
Graphic::None(_) | Graphic::NoneList(_) => continue,
Graphic::Color(item) => solid_fill(scene, item.element()),
Graphic::ColorList(list) => {
let Some(color) = list.element(0) else { continue };
solid_fill(scene, color);
}
Graphic::Color(item) => solid_fill(scene, ItemRef::Item(item)),
Graphic::ColorList(list) => solid_fill(scene, ItemRef::ListItem(list, 0)),
Graphic::Gradient(item) => gradient_fill(scene, ItemRef::Item(item)),
Graphic::GradientList(list) => gradient_fill(scene, ItemRef::ListItem(list, 0)),
// Any other graphic content paints as a texture clipped to the path
Expand Down Expand Up @@ -1839,13 +1861,16 @@ fn render_vector_item_to_vello(
continue;
};

let solid_stroke = |scene: &mut Scene, color: &Color| {
let brush = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color());
let solid_stroke = |scene: &mut Scene, item: ItemRef<'_, Color>| {
let Some(color) = item.element() else { return };
let color = color.with_alpha(color.a() * item.paint_opacity(render_params.for_mask));

let brush = peniko::Brush::Solid(SRGBA8::from(color).to_peniko_color());

scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, None, &path);
};
let gradient_stroke = |scene: &mut Scene, gradient_item: ItemRef<'_, Gradient>| {
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(gradient_item, &multiplied_transform) else {
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(gradient_item, &multiplied_transform, render_params.for_mask) else {
return;
};
let inverse_element_transform = if transform_is_invertible(element_transform) {
Expand All @@ -1860,11 +1885,8 @@ fn render_vector_item_to_vello(

match stroke_graphic {
Graphic::None(_) | Graphic::NoneList(_) => continue,
Graphic::Color(item) => solid_stroke(scene, item.element()),
Graphic::ColorList(list) => {
let Some(color) = list.element(0) else { continue };
solid_stroke(scene, color);
}
Graphic::Color(item) => solid_stroke(scene, ItemRef::Item(item)),
Graphic::ColorList(list) => solid_stroke(scene, ItemRef::ListItem(list, 0)),
Graphic::Gradient(item) => gradient_stroke(scene, ItemRef::Item(item)),
Graphic::GradientList(list) => gradient_stroke(scene, ItemRef::ListItem(list, 0)),
// Any other graphic content paints as a texture clipped to the stroked region
Expand Down
Loading