mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-28 00:38:11 +08:00
Add a Clear gradient spread that cuts off to transparency beyond the ends (#4410)
This commit is contained in:
committed by
Dennis Kobert
parent
1d250f6d51
commit
3f70e28948
@@ -569,7 +569,7 @@ impl ColorPickerMessageHandler {
|
|||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
TextLabel::new("HSV")
|
TextLabel::new("HSV")
|
||||||
.tooltip_label("Hue/Saturation/Value")
|
.tooltip_label("Hue/Saturation/Value")
|
||||||
.tooltip_description("Also known as Hue/Saturation/Brightness (HSB). Not to be confused with Hue/Saturation/Lightness (HSL), a different color model.")
|
.tooltip_description("Also known as Hue/Saturation/Brightness (HSB), but distinct from Hue/Saturation/Lightness (HSL), a different color model.")
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
hsv_input(
|
hsv_input(
|
||||||
@@ -685,12 +685,7 @@ const HUE_DESCRIPTION: &str = "The shade along the spectrum of the rainbow.";
|
|||||||
const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color.";
|
const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color.";
|
||||||
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
||||||
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
||||||
const ENDS_DESCRIPTION: &str = "\
|
const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends.";
|
||||||
How the gradient continues beyond its ends:\n\
|
|
||||||
**Pad** extends the end colors outward.\n\
|
|
||||||
**Reflect** loops the gradient by mirroring back-and-forth.\n\
|
|
||||||
**Repeat** loops the gradient as copies of itself.\
|
|
||||||
";
|
|
||||||
|
|
||||||
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
|
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
|
||||||
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
|
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::renderer::{RenderParams, format_transform_matrix, gradient_placement, transform_is_invertible};
|
use crate::renderer::{ClearGuardPlacement, RenderParams, format_transform_matrix, gradient_placement, spread_adjusted_samples, transform_is_invertible};
|
||||||
use crate::{Render, RenderSvgSegmentList, SvgRender};
|
use crate::{Render, RenderSvgSegmentList, SvgRender};
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use core_types::attribute::Transform;
|
use core_types::attribute::Transform;
|
||||||
@@ -112,7 +112,9 @@ pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>
|
|||||||
let local_gradient_transform: DAffine2 = source.attr::<Transform>(0);
|
let local_gradient_transform: DAffine2 = source.attr::<Transform>(0);
|
||||||
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(0);
|
let gradient_spread: GradientSpread = source.attr::<GradientSpreadAttr>(0);
|
||||||
|
|
||||||
for (position, color, original_midpoint) in stops.interpolated_samples() {
|
let (samples, _) = spread_adjusted_samples(stops, gradient_spread, gradient_form, ClearGuardPlacement::SvgStopOrder);
|
||||||
|
|
||||||
|
for (position, color, original_midpoint) in samples {
|
||||||
stop.push_str("<stop");
|
stop.push_str("<stop");
|
||||||
if position != 0. {
|
if position != 0. {
|
||||||
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
||||||
@@ -149,7 +151,7 @@ pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>
|
|||||||
format!(r#" gradientTransform="{gradient_transform}""#)
|
format!(r#" gradientTransform="{gradient_transform}""#)
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradient_spread = if gradient_spread == GradientSpread::Pad {
|
let gradient_spread = if matches!(gradient_spread, GradientSpread::Pad | GradientSpread::Clear) {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
format!(r#" spreadMethod="{}""#, gradient_spread.svg_name())
|
format!(r#" spreadMethod="{}""#, gradient_spread.svg_name())
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use graphic_types::graphic::{PaintColumns, PaintOverlay, PaintReach, has_paint,
|
|||||||
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
|
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
|
||||||
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
||||||
use graphic_types::vector_types::gradient::{Gradient, GradientForm};
|
use graphic_types::vector_types::gradient::{Gradient, GradientForm};
|
||||||
use graphic_types::vector_types::markers::{GradientForm as GradientFormAttr, GradientSpread as GradientSpreadAttr};
|
use graphic_types::vector_types::markers::{GradientSpread as GradientSpreadAttr, GradientForm as GradientFormAttr};
|
||||||
use graphic_types::vector_types::subpath::Subpath;
|
use graphic_types::vector_types::subpath::Subpath;
|
||||||
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
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::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
@@ -400,11 +400,79 @@ pub(crate) fn gradient_placement(transform: DAffine2, gradient_form: GradientFor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Texel count of the baked gradient ramp Vello samples stops through (`N_SAMPLES`/`GRADIENT_WIDTH` in vello_encoding).
|
||||||
|
const VELLO_GRADIENT_RAMP_TEXELS: f64 = 512.;
|
||||||
|
|
||||||
|
/// Renderable gradient samples of `(position, color, original midpoint)`, as produced by [`Gradient::interpolated_samples`].
|
||||||
|
type GradientSamples = Vec<(f64, Color, Option<f64>)>;
|
||||||
|
|
||||||
|
/// Where a renderer needs the transparent guard stops that emulate the `Clear` spread, which neither SVG nor Vello supports natively.
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
pub(crate) enum ClearGuardPlacement {
|
||||||
|
/// Guards share the range ends' exact offsets, resolved against the visible colors by stop order alone.
|
||||||
|
SvgStopOrder,
|
||||||
|
/// Guards own the outermost ramp texel at each cleared end, since Vello's pad extension samples those texels for
|
||||||
|
/// everything beyond the ends and its ramp bake would tie-break a shared-offset guard away. The visible range
|
||||||
|
/// compresses inward by one texel per cleared end, costing about 0.4% of the ramp's color resolution.
|
||||||
|
VelloRampTexels,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The gradient's renderable samples plus the gradient-space span `(start, end)` the renderer's 0 to 1 offset range must cover, normally the unit interval with the samples unchanged.
|
||||||
|
///
|
||||||
|
/// The `Clear` spread brackets the samples with transparent guard stops placed per `guards`: the pad extension then
|
||||||
|
/// paints transparency outward while hard stops cut the paint off exactly at the unit range's boundaries. A radial
|
||||||
|
/// gradient's span still starts at zero, since its sampling distance never goes below the center.
|
||||||
|
pub(crate) fn spread_adjusted_samples(gradient: &Gradient, gradient_spread: GradientSpread, gradient_form: GradientForm, guards: ClearGuardPlacement) -> (GradientSamples, (f64, f64)) {
|
||||||
|
let samples = gradient.interpolated_samples();
|
||||||
|
if gradient_spread != GradientSpread::Clear {
|
||||||
|
return (samples, (0., 1.));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The remapped offsets where the visible range's ends land, with the guards owning whatever lies outside them
|
||||||
|
let texel = 1. / (VELLO_GRADIENT_RAMP_TEXELS - 1.);
|
||||||
|
let (start_offset, end_offset) = match (guards, gradient_form) {
|
||||||
|
(ClearGuardPlacement::SvgStopOrder, _) => (0., 1.),
|
||||||
|
(ClearGuardPlacement::VelloRampTexels, GradientForm::Linear) => (texel, 1. - texel),
|
||||||
|
(ClearGuardPlacement::VelloRampTexels, GradientForm::Radial) => (0., 1. - texel),
|
||||||
|
};
|
||||||
|
let remap = |position: f64| (1. - position) * start_offset + position * end_offset;
|
||||||
|
|
||||||
|
// The geometric span grows to compensate for the compression, keeping the visible range at the unit interval
|
||||||
|
let scale = 1. / (end_offset - start_offset);
|
||||||
|
let span = (-start_offset * scale, (1. - start_offset) * scale);
|
||||||
|
|
||||||
|
// A stopless gradient paints solid black, matching `Gradient::evaluate`
|
||||||
|
let first_color = samples.first().map_or(Color::BLACK, |&(_, color, _)| color);
|
||||||
|
let last_color = samples.last().map_or(Color::BLACK, |&(_, color, _)| color);
|
||||||
|
let needs_start_anchor = samples.first().is_none_or(|&(position, ..)| position > 0.);
|
||||||
|
let needs_end_anchor = samples.last().is_none_or(|&(position, ..)| position < 1.);
|
||||||
|
|
||||||
|
let mut adjusted = Vec::with_capacity(samples.len() + 4);
|
||||||
|
|
||||||
|
// Lead with the transparent guard (linear only, a radial's center is already the sampling minimum), then anchor the visible range's start color
|
||||||
|
if gradient_form == GradientForm::Linear {
|
||||||
|
adjusted.push((0., Color::TRANSPARENT, None));
|
||||||
|
}
|
||||||
|
if needs_start_anchor {
|
||||||
|
adjusted.push((remap(0.), first_color, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
adjusted.extend(samples.into_iter().map(|(position, color, midpoint)| (remap(position), color, midpoint)));
|
||||||
|
|
||||||
|
// Anchor the visible range's end color, then cut to the trailing transparent guard
|
||||||
|
if needs_end_anchor {
|
||||||
|
adjusted.push((remap(1.), last_color, None));
|
||||||
|
}
|
||||||
|
adjusted.push((1., Color::TRANSPARENT, None));
|
||||||
|
|
||||||
|
(adjusted, span)
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts a gradient's renderer samples to peniko color stops, duplicating an off-zero first stop at position 0 since Vello ignores the first stop's position and always treats it as 0.
|
/// Converts a gradient's renderer samples to peniko color stops, duplicating an off-zero first stop at position 0 since Vello ignores the first stop's position and always treats it as 0.
|
||||||
fn peniko_color_stops(gradient: &Gradient) -> peniko::ColorStops {
|
fn peniko_color_stops(samples: &[(f64, Color, Option<f64>)]) -> peniko::ColorStops {
|
||||||
let mut peniko_stops = peniko::ColorStops::new();
|
let mut peniko_stops = peniko::ColorStops::new();
|
||||||
|
|
||||||
for (position, color, _) in gradient.interpolated_samples() {
|
for &(position, color, _) in samples {
|
||||||
let color = peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color());
|
let color = peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color());
|
||||||
|
|
||||||
if peniko_stops.is_empty() && position > 0. {
|
if peniko_stops.is_empty() && position > 0. {
|
||||||
@@ -425,6 +493,16 @@ fn peniko_color_stops(gradient: &Gradient) -> peniko::ColorStops {
|
|||||||
peniko_stops
|
peniko_stops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The peniko extend mode for a spread; `Clear` rides pad, with the transparent guard stops from `spread_adjusted_samples` doing the clearing.
|
||||||
|
fn peniko_extend(gradient_spread: GradientSpread) -> peniko::Extend {
|
||||||
|
match gradient_spread {
|
||||||
|
GradientSpread::Pad | GradientSpread::Clear => peniko::Extend::Pad,
|
||||||
|
GradientSpread::Reflect => peniko::Extend::Reflect,
|
||||||
|
GradientSpread::Repeat => peniko::Extend::Repeat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
|
fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> {
|
||||||
let stops = gradient_list.element(0)?;
|
let stops = gradient_list.element(0)?;
|
||||||
|
|
||||||
@@ -432,10 +510,11 @@ fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list
|
|||||||
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
|
let gradient_transform: DAffine2 = gradient_list.attr::<Transform>(0);
|
||||||
let gradient_spread: GradientSpread = gradient_list.attr::<GradientSpreadAttr>(0);
|
let gradient_spread: GradientSpread = gradient_list.attr::<GradientSpreadAttr>(0);
|
||||||
|
|
||||||
let peniko_stops = peniko_color_stops(stops);
|
let (samples, span) = spread_adjusted_samples(stops, gradient_spread, gradient_form, ClearGuardPlacement::VelloRampTexels);
|
||||||
|
let peniko_stops = peniko_color_stops(&samples);
|
||||||
|
|
||||||
// The unit gradient is placed by the desheared frame so a non-uniform transform produces the intended ellipse
|
// The unit gradient is placed by the desheared frame so a non-uniform transform produces the intended ellipse
|
||||||
let (start, end, gradient_to_device) = (DVec2::ZERO, DVec2::X, gradient_placement(multiplied_transform * gradient_transform, gradient_form));
|
let (start, end, gradient_to_device) = (DVec2::X * span.0, DVec2::X * span.1, gradient_placement(multiplied_transform * gradient_transform, gradient_form));
|
||||||
|
|
||||||
let brush = peniko::Brush::Gradient(peniko::Gradient {
|
let brush = peniko::Brush::Gradient(peniko::Gradient {
|
||||||
kind: match gradient_form {
|
kind: match gradient_form {
|
||||||
@@ -452,11 +531,7 @@ fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list
|
|||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
},
|
},
|
||||||
extend: match gradient_spread {
|
extend: peniko_extend(gradient_spread),
|
||||||
GradientSpread::Pad => peniko::Extend::Pad,
|
|
||||||
GradientSpread::Reflect => peniko::Extend::Reflect,
|
|
||||||
GradientSpread::Repeat => peniko::Extend::Repeat,
|
|
||||||
},
|
|
||||||
stops: peniko_stops,
|
stops: peniko_stops,
|
||||||
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
|
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -2344,8 +2419,10 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
|
|||||||
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (samples, _) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, ClearGuardPlacement::SvgStopOrder);
|
||||||
|
|
||||||
let mut stop_string = String::new();
|
let mut stop_string = String::new();
|
||||||
for (position, color, original_midpoint) in gradient.interpolated_samples() {
|
for (position, color, original_midpoint) in samples {
|
||||||
let _ = write!(stop_string, r##"<stop offset="{}" stop-color="#{}""##, position, SRGBA8::from(color).to_rgb_hex());
|
let _ = write!(stop_string, r##"<stop offset="{}" stop-color="#{}""##, position, SRGBA8::from(color).to_rgb_hex());
|
||||||
if color.a() < 1. {
|
if color.a() < 1. {
|
||||||
let _ = write!(stop_string, r#" stop-opacity="{}""#, color.a());
|
let _ = write!(stop_string, r#" stop-opacity="{}""#, color.a());
|
||||||
@@ -2366,7 +2443,7 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
|
|||||||
};
|
};
|
||||||
|
|
||||||
let gradient_id = generate_uuid();
|
let gradient_id = generate_uuid();
|
||||||
let spread_method_attribute = if gradient_spread == GradientSpread::Pad {
|
let spread_method_attribute = if matches!(gradient_spread, GradientSpread::Pad | GradientSpread::Clear) {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
format!(r#" spreadMethod="{}""#, gradient_spread.svg_name())
|
format!(r#" spreadMethod="{}""#, gradient_spread.svg_name())
|
||||||
@@ -2422,27 +2499,24 @@ fn render_gradient_vello<S: LaneSource<Element = Gradient>>(source: &S, scene: &
|
|||||||
let blend_mode = blend_mode_attr.to_peniko();
|
let blend_mode = blend_mode_attr.to_peniko();
|
||||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||||
|
|
||||||
let stops = peniko_color_stops(gradient);
|
let (samples, span) = spread_adjusted_samples(gradient, gradient_spread, gradient_form, ClearGuardPlacement::VelloRampTexels);
|
||||||
|
let stops = peniko_color_stops(&samples);
|
||||||
|
|
||||||
let extend = match gradient_spread {
|
let extend = peniko_extend(gradient_spread);
|
||||||
GradientSpread::Pad => peniko::Extend::Pad,
|
|
||||||
GradientSpread::Reflect => peniko::Extend::Reflect,
|
|
||||||
GradientSpread::Repeat => peniko::Extend::Repeat,
|
|
||||||
};
|
|
||||||
|
|
||||||
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied.
|
// 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.
|
// 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_form {
|
let kind = match gradient_form {
|
||||||
GradientForm::Linear => peniko::LinearGradientPosition {
|
GradientForm::Linear => peniko::LinearGradientPosition {
|
||||||
start: to_point(DVec2::ZERO),
|
start: to_point(DVec2::X * span.0),
|
||||||
end: to_point(DVec2::X),
|
end: to_point(DVec2::X * span.1),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
GradientForm::Radial => peniko::RadialGradientPosition {
|
GradientForm::Radial => peniko::RadialGradientPosition {
|
||||||
start_center: to_point(DVec2::ZERO),
|
start_center: to_point(DVec2::ZERO),
|
||||||
start_radius: 0.,
|
start_radius: 0.,
|
||||||
end_center: to_point(DVec2::ZERO),
|
end_center: to_point(DVec2::ZERO),
|
||||||
end_radius: 1.,
|
end_radius: span.1 as f32,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
};
|
};
|
||||||
@@ -3161,3 +3235,51 @@ mod group_walk_tests {
|
|||||||
assert_eq!(native_outlines, legacy);
|
assert_eq!(native_outlines, legacy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod spread_tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn spread_adjusted_samples_wraps_clear_in_transparent_guards() {
|
||||||
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
|
||||||
|
let (samples, span) = spread_adjusted_samples(&gradient, GradientSpread::Repeat, GradientForm::Linear, ClearGuardPlacement::SvgStopOrder);
|
||||||
|
assert_eq!(span, (0., 1.));
|
||||||
|
assert_eq!(samples, gradient.interpolated_samples());
|
||||||
|
|
||||||
|
// SVG guards share the range ends' exact offsets, ordered so the pad extension resolves to the transparent outer stops
|
||||||
|
let (samples, span) = spread_adjusted_samples(&gradient, GradientSpread::Clear, GradientForm::Linear, ClearGuardPlacement::SvgStopOrder);
|
||||||
|
assert_eq!(span, (0., 1.));
|
||||||
|
assert_eq!(
|
||||||
|
samples,
|
||||||
|
vec![(0., Color::TRANSPARENT, None), (0., Color::BLACK, None), (1., Color::WHITE, None), (1., Color::TRANSPARENT, None)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Vello guards own the outermost ramp texels, with the visible range compressed inward to make room
|
||||||
|
let texel = 1. / (VELLO_GRADIENT_RAMP_TEXELS - 1.);
|
||||||
|
let (samples, span) = spread_adjusted_samples(&gradient, GradientSpread::Clear, GradientForm::Linear, ClearGuardPlacement::VelloRampTexels);
|
||||||
|
assert_eq!(
|
||||||
|
samples,
|
||||||
|
vec![
|
||||||
|
(0., Color::TRANSPARENT, None),
|
||||||
|
(texel, Color::BLACK, None),
|
||||||
|
(1. - texel, Color::WHITE, None),
|
||||||
|
(1., Color::TRANSPARENT, None)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(span.0 < 0. && span.1 > 1., "the geometry must stretch to compensate for the compressed stops: {span:?}");
|
||||||
|
|
||||||
|
// A radial keeps its stops and span anchored at zero, with no guard below the center
|
||||||
|
let (samples, span) = spread_adjusted_samples(&gradient, GradientSpread::Clear, GradientForm::Radial, ClearGuardPlacement::VelloRampTexels);
|
||||||
|
assert_eq!(span.0, 0.);
|
||||||
|
assert_eq!(samples.first().unwrap(), &(0., Color::BLACK, None));
|
||||||
|
assert_eq!(samples.last().unwrap(), &(1., Color::TRANSPARENT, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spread_adjusted_samples_keeps_a_stopless_clear_gradient_black_inside_the_range() {
|
||||||
|
let (samples, _) = spread_adjusted_samples(&Gradient::from(Vec::new()), GradientSpread::Clear, GradientForm::Linear, ClearGuardPlacement::SvgStopOrder);
|
||||||
|
let colors: Vec<Color> = samples.iter().map(|&(_, color, _)| color).collect();
|
||||||
|
assert_eq!(colors, vec![Color::TRANSPARENT, Color::BLACK, Color::BLACK, Color::TRANSPARENT]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -612,6 +612,12 @@ impl Gradient {
|
|||||||
let cycle = t.rem_euclid(2.);
|
let cycle = t.rem_euclid(2.);
|
||||||
if cycle > 1. { 2. - cycle } else { cycle }
|
if cycle > 1. { 2. - cycle } else { cycle }
|
||||||
}
|
}
|
||||||
|
GradientSpread::Clear => {
|
||||||
|
if !(0. ..=1.).contains(&t) {
|
||||||
|
return Color::TRANSPARENT;
|
||||||
|
}
|
||||||
|
t
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let stops = self.normalized_stops();
|
let stops = self.normalized_stops();
|
||||||
@@ -798,7 +804,9 @@ pub enum GradientSpread {
|
|||||||
/// Loops the gradient as copies of itself.
|
/// Loops the gradient as copies of itself.
|
||||||
#[icon("GradientSpreadRepeat")]
|
#[icon("GradientSpreadRepeat")]
|
||||||
Repeat,
|
Repeat,
|
||||||
// TODO: Add a "Clear" variant that returns transparent black outside the gradient's range
|
/// Cuts off to transparency beyond the ends.
|
||||||
|
#[icon("GradientSpreadClear")]
|
||||||
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GradientSpread {
|
impl GradientSpread {
|
||||||
@@ -807,6 +815,8 @@ impl GradientSpread {
|
|||||||
GradientSpread::Pad => "pad",
|
GradientSpread::Pad => "pad",
|
||||||
GradientSpread::Reflect => "reflect",
|
GradientSpread::Reflect => "reflect",
|
||||||
GradientSpread::Repeat => "repeat",
|
GradientSpread::Repeat => "repeat",
|
||||||
|
// SVG has no clear mode; renderers emulate it over pad with transparent guard stops
|
||||||
|
GradientSpread::Clear => "pad",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,6 +958,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_spread_evaluates_to_transparency_outside_the_unit_range() {
|
||||||
|
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||||
|
|
||||||
|
assert_eq!(gradient.evaluate(-0.25, GradientSpread::Clear), Color::TRANSPARENT);
|
||||||
|
assert_eq!(gradient.evaluate(1.25, GradientSpread::Clear), Color::TRANSPARENT);
|
||||||
|
|
||||||
|
for t in [0., 0.25, 1.] {
|
||||||
|
assert_eq!(
|
||||||
|
gradient.evaluate(t, GradientSpread::Clear),
|
||||||
|
gradient.evaluate(t, GradientSpread::Pad),
|
||||||
|
"inside the range Clear must match Pad at t = {t}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gradient_ui_write_back_elides_default_attributes() {
|
fn gradient_ui_write_back_elides_default_attributes() {
|
||||||
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
use core_types::attribute::Attribute;
|
use core_types::attribute::Attribute;
|
||||||
|
|
||||||
core_types::attribute! {
|
core_types::attribute! {
|
||||||
/// Gradient's spread behavior past its endpoints (`Pad`, `Reflect`, or `Repeat`).
|
/// Gradient's spread behavior past its endpoints (`Pad`, `Reflect`, `Repeat`, or `Clear`).
|
||||||
pub GradientSpread("gradient_spread"): crate::gradient::GradientSpread;
|
pub GradientSpread("gradient_spread"): crate::gradient::GradientSpread;
|
||||||
/// Gradient's shape (`Linear` or `Radial`).
|
/// Gradient's shape (`Linear` or `Radial`).
|
||||||
pub GradientForm("gradient_form"): crate::gradient::GradientForm;
|
pub GradientForm("gradient_form"): crate::gradient::GradientForm;
|
||||||
|
|||||||
@@ -1209,7 +1209,7 @@ fn gradient_form(_: impl Ctx, gradient: Gradient, gradient_form: vector_types::G
|
|||||||
(gradient, Attr(gradient_form))
|
(gradient, Attr(gradient_form))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets how each gradient in the input list extends past its endpoints: Pad, Reflect, or Repeat.
|
/// Sets how each gradient in the input list extends past its endpoints: Pad, Reflect, Repeat, or Clear.
|
||||||
#[node_macro::node(category("Gradient"))]
|
#[node_macro::node(category("Gradient"))]
|
||||||
fn gradient_spread(_: impl Ctx, gradient: Gradient, gradient_spread: vector_types::GradientSpread) -> (Gradient, Attr<GradientSpreadAttr>) {
|
fn gradient_spread(_: impl Ctx, gradient: Gradient, gradient_spread: vector_types::GradientSpread) -> (Gradient, Attr<GradientSpreadAttr>) {
|
||||||
(gradient, Attr(gradient_spread))
|
(gradient, Attr(gradient_spread))
|
||||||
@@ -1237,7 +1237,7 @@ fn gradient_midpoints(_: impl Ctx, mut gradient: Gradient, midpoints: List<f64>)
|
|||||||
gradient
|
gradient
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `gradient_spread` attribute: Pad (default), Reflect, or Repeat.
|
/// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `gradient_spread` attribute: Pad (default), Reflect, Repeat, or Clear.
|
||||||
#[node_macro::node(category("Color"))]
|
#[node_macro::node(category("Color"))]
|
||||||
fn sample_gradient(
|
fn sample_gradient(
|
||||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||||
|
|||||||
Reference in New Issue
Block a user