Add a gradient interpolation space attribute with sRGB Gamma (existing) and sRGB Linear (new) (#4412)

* Build dropdown menu entries from choice type metadata

* Add a gradient interpolation color space attribute, blending stops in linear light by default

* Add a Space interpolation dropdown to the color picker popover

* Stamp legacy gradient ramps with their implicit gamma interpolation during deserialization

* Resolve color-interpolation from SVG style blocks and fix cascade order among repeated declarations
This commit is contained in:
Keavon Chambers
2026-08-05 15:58:25 -07:00
committed by GitHub
parent 0b922f1452
commit fd01d66070
29 changed files with 847 additions and 123 deletions

View File

@@ -555,6 +555,7 @@ tagged_value! {
GradientForm(vector::style::GradientForm),
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
GradientSpread(vector::style::GradientSpread),
GradientInterpolation(vector::style::GradientInterpolation),
ReferencePoint(vector::ReferencePoint),
CentroidType(vector::misc::CentroidType),
BooleanOperation(vector::misc::BooleanOperation),
@@ -805,11 +806,15 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
.and_then(|c| c.get("element").or_else(|| c.get("instance")).or_else(|| c.get("instances")))
.and_then(|element| element.as_array());
// An empty legacy table wrapper carries no gradient, degrading to the default rather than failing the document load
// An empty legacy table wrapper carries no gradient, degrading to the default (in the era's gamma) rather than failing the document load
if let Some(array) = table_element
&& array.is_empty()
{
return Ok(MemoHash::new(TaggedValue::GradientRamp(GradientRamp::default())));
let ramp = GradientRamp {
gradient_interpolation: vector::style::GradientInterpolation::SrgbGamma,
..Default::default()
};
return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp)));
}
let payload = table_element.and_then(|array| array.first()).unwrap_or(content);
@@ -1023,7 +1028,7 @@ mod paint_default_parsing {
#[cfg(test)]
mod gradient_shape_migration {
use graphic_types::vector_types::GradientSpread;
use graphic_types::vector_types::{GradientInterpolation, GradientSpread};
use super::*;
@@ -1050,9 +1055,29 @@ mod gradient_shape_migration {
let json = serde_json::to_value(&value).unwrap();
assert!(json.get("GradientRamp").and_then(|payload| payload.get("stops")).is_some(), "the payload should nest its stops: {json}");
assert_eq!(
json.get("GradientRamp").and_then(|payload| payload.get("gradient_interpolation")),
Some(&serde_json::json!("SrgbLinear")),
"the interpolation should serialize even at its default, marking the ramp as post-legacy: {json}"
);
assert_eq!(load(json), value);
}
// TODO: Eventually remove this document upgrade code
#[test]
fn ramp_without_interpolation_field_reads_as_legacy_gamma() {
let json = serde_json::json!({ "GradientRamp": { "stops": { "color": [white(), white()] } } });
let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the ramp payload should become a gradient ramp value")
};
assert_eq!(
ramp.gradient_interpolation,
GradientInterpolation::SrgbGamma,
"a ramp saved before the field existed should read as gamma"
);
}
// TODO: Eventually remove this document upgrade code
#[test]
fn legacy_flat_stops_parse_faithfully() {
@@ -1060,6 +1085,7 @@ mod gradient_shape_migration {
let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the flat stops should become a gradient ramp value")
};
assert_eq!(ramp.gradient_interpolation, GradientInterpolation::SrgbGamma, "the pre-ramp flat form should carry the era's gamma");
let gradient = Gradient::from(ramp);
assert_eq!(gradient.positions(), vec![0., 0.25]);
@@ -1073,6 +1099,7 @@ mod gradient_shape_migration {
let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the tuple stops should become a gradient ramp value")
};
assert_eq!(ramp.gradient_interpolation, GradientInterpolation::SrgbGamma, "the pre-ramp tuple form should carry the era's gamma");
let gradient = Gradient::from(ramp);
assert_eq!(gradient.positions(), vec![0., 1.]);
@@ -1083,7 +1110,11 @@ mod gradient_shape_migration {
#[test]
fn empty_legacy_gradient_table_degrades_to_the_default() {
let json = serde_json::json!({ "GradientTable": { "element": [] } });
assert_eq!(load(json), TaggedValue::GradientRamp(GradientRamp::default()));
let expected = GradientRamp {
gradient_interpolation: GradientInterpolation::SrgbGamma,
..Default::default()
};
assert_eq!(load(json), TaggedValue::GradientRamp(expected));
}
// TODO: Eventually remove this document upgrade code