From 16470c77ec498f6d06b9e633a19ab92885083954 Mon Sep 17 00:00:00 2001 From: Tasuku Suzuki Date: Thu, 23 Oct 2025 23:06:18 +0900 Subject: [PATCH 1/2] Support arbitrary angle ranges in conic-gradient Normalize conic gradient stop positions to [0,1] range using modulo wrapping, enabling support for angle ranges outside 0-360 degrees (e.g., -90deg to 270deg or 360deg to 720deg). Key changes: - Separate duplicate positions with different colors to prevent flickering - Interpolate boundary colors between min/max stops for seamless gradients - Add boundary stops at 0.0 and 1.0 when needed Tests cover negative angles, 360deg+ offsets, and full 360deg range with different start/end colors. --- internal/core/graphics/brush.rs | 76 +++++++++++++++++- .../software/basic/conic-gradients.slint | 8 ++ .../software/basic/conic-gradients.png | Bin 5070 -> 5806 bytes 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/internal/core/graphics/brush.rs b/internal/core/graphics/brush.rs index 0f0f6c61acc..1209d40620a 100644 --- a/internal/core/graphics/brush.rs +++ b/internal/core/graphics/brush.rs @@ -259,10 +259,80 @@ pub struct ConicGradientBrush(SharedVector); impl ConicGradientBrush { /// Creates a new conic gradient with the provided color stops. - /// The stops should have angle positions in the range 0.0 to 1.0, - /// where 0.0 is 0 degrees (north) and 1.0 is 360 degrees. + /// + /// Positions can be any value (including negative or > 1.0) and will be automatically + /// normalized to the range [0.0, 1.0], where 0.0 represents 0° (north) and 1.0 represents 360°. + /// + /// If the provided stops don't span the full [0, 1] range, boundary stops at 0.0 and 1.0 + /// will be automatically added with interpolated colors to create a seamless circular gradient. pub fn new(stops: impl IntoIterator) -> Self { - Self(stops.into_iter().collect()) + const EPSILON: f32 = 0.0001; + + /// Helper: Linearly interpolate between two colors + fn interpolate_color(c1: Color, c2: Color, t: f32) -> Color { + let argb1 = c1.to_argb_u8(); + let argb2 = c2.to_argb_u8(); + Color::from_argb_u8( + ((1.0 - t) * argb1.alpha as f32 + t * argb2.alpha as f32) as u8, + ((1.0 - t) * argb1.red as f32 + t * argb2.red as f32) as u8, + ((1.0 - t) * argb1.green as f32 + t * argb2.green as f32) as u8, + ((1.0 - t) * argb1.blue as f32 + t * argb2.blue as f32) as u8, + ) + } + + let mut stops: alloc::vec::Vec<_> = stops.into_iter().collect(); + + if !stops.is_empty() { + // All backends (Qt, Software, FemtoVG, Skia) require positions in [0, 1] + + // Check if stops already span the full range [0, 1] + let min_pos = stops.iter().map(|s| s.position).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); + let max_pos = stops.iter().map(|s| s.position).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); + + if min_pos >= 0.0 && max_pos <= 1.0 { + // Stops already span [0, 1], can be passed to backends as-is + } else { + // Need to normalize and add boundary stops + + // 1. Normalize all positions to [0, 1) range by wrapping + for s in &mut stops { + s.position = s.position - s.position.floor(); + } + + // 2. Separate duplicate positions with different colors to avoid flickering + // This prevents two different colors from rendering at the same position during rotation + // Check circularly to handle wrapping at 0/1 boundary + for i in 0..stops.len() { + let j = (i + 1) % stops.len(); + if (stops[i].position - stops[j].position).abs() < EPSILON && stops[i].color != stops[j].color { + stops[i].position = (stops[i].position - EPSILON).max(0.0); + stops[j].position = (stops[j].position + EPSILON).min(1.0); + } + } + + // 3. Calculate color at 0/1 boundary by interpolating between min and max stops + // For seamless circular gradient, 0.0 and 1.0 must have the same color + let max_stop = stops.iter().max_by(|a, b| a.position.partial_cmp(&b.position).unwrap()).unwrap(); + let min_stop = stops.iter().min_by(|a, b| a.position.partial_cmp(&b.position).unwrap()).unwrap(); + + let boundary_color = { + let gap = 1.0 - max_stop.position + min_stop.position; + if gap > EPSILON { + let t = (1.0 - max_stop.position) / gap; + interpolate_color(max_stop.color, min_stop.color, t) + } else { + max_stop.color + } + }; + + // 4. Sort stops by position and add boundary stops at 0 and 1 + stops.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)); + stops.insert(0, GradientStop { position: 0.0, color: boundary_color }); + stops.push(GradientStop { position: 1.0, color: boundary_color }); + } + } + + Self(SharedVector::from_iter(stops.into_iter())) } /// Returns the color stops of the conic gradient. diff --git a/tests/screenshots/cases/software/basic/conic-gradients.slint b/tests/screenshots/cases/software/basic/conic-gradients.slint index 2525a2cb7c0..26092393ebd 100644 --- a/tests/screenshots/cases/software/basic/conic-gradients.slint +++ b/tests/screenshots/cases/software/basic/conic-gradients.slint @@ -33,6 +33,14 @@ export component TestCase inherits Window { // Edge case: invisible stop at start Rectangle { background: @conic-gradient(transparent 0deg, red 3.6deg, white 180deg, transparent 360deg); } } + Row { + // Negative angle range (-90deg ~ 270deg) + Rectangle { background: @conic-gradient(red 90deg * -1, green 30deg, blue 150deg, red 270deg); } + // 360deg offset range (360deg ~ 720deg) + Rectangle { background: @conic-gradient(cyan 360deg, magenta 480deg, yellow 600deg, cyan 720deg); } + // 360deg range with different start/end colors (90deg ~ 450deg) + Rectangle { background: @conic-gradient(orange 90deg, pink 270deg, purple 450deg); } + } } init => { diff --git a/tests/screenshots/references/software/basic/conic-gradients.png b/tests/screenshots/references/software/basic/conic-gradients.png index 30fe16f9d52106b2e1cc421a8ea38a36e821df4e..4087cc9a00dafd740a42a3ee18cf3850fddd82f8 100644 GIT binary patch delta 5664 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 F|36+YZR-F4 literal 5070 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 Date: Thu, 23 Oct 2025 14:13:44 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- internal/core/graphics/brush.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/core/graphics/brush.rs b/internal/core/graphics/brush.rs index 1209d40620a..f481f1d5733 100644 --- a/internal/core/graphics/brush.rs +++ b/internal/core/graphics/brush.rs @@ -286,8 +286,10 @@ impl ConicGradientBrush { // All backends (Qt, Software, FemtoVG, Skia) require positions in [0, 1] // Check if stops already span the full range [0, 1] - let min_pos = stops.iter().map(|s| s.position).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); - let max_pos = stops.iter().map(|s| s.position).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); + let min_pos = + stops.iter().map(|s| s.position).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); + let max_pos = + stops.iter().map(|s| s.position).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); if min_pos >= 0.0 && max_pos <= 1.0 { // Stops already span [0, 1], can be passed to backends as-is @@ -304,7 +306,9 @@ impl ConicGradientBrush { // Check circularly to handle wrapping at 0/1 boundary for i in 0..stops.len() { let j = (i + 1) % stops.len(); - if (stops[i].position - stops[j].position).abs() < EPSILON && stops[i].color != stops[j].color { + if (stops[i].position - stops[j].position).abs() < EPSILON + && stops[i].color != stops[j].color + { stops[i].position = (stops[i].position - EPSILON).max(0.0); stops[j].position = (stops[j].position + EPSILON).min(1.0); } @@ -312,8 +316,14 @@ impl ConicGradientBrush { // 3. Calculate color at 0/1 boundary by interpolating between min and max stops // For seamless circular gradient, 0.0 and 1.0 must have the same color - let max_stop = stops.iter().max_by(|a, b| a.position.partial_cmp(&b.position).unwrap()).unwrap(); - let min_stop = stops.iter().min_by(|a, b| a.position.partial_cmp(&b.position).unwrap()).unwrap(); + let max_stop = stops + .iter() + .max_by(|a, b| a.position.partial_cmp(&b.position).unwrap()) + .unwrap(); + let min_stop = stops + .iter() + .min_by(|a, b| a.position.partial_cmp(&b.position).unwrap()) + .unwrap(); let boundary_color = { let gap = 1.0 - max_stop.position + min_stop.position; @@ -326,7 +336,9 @@ impl ConicGradientBrush { }; // 4. Sort stops by position and add boundary stops at 0 and 1 - stops.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)); + stops.sort_by(|a, b| { + a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal) + }); stops.insert(0, GradientStop { position: 0.0, color: boundary_color }); stops.push(GradientStop { position: 1.0, color: boundary_color }); }