diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fa9cf0..66a730d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - 2-point conical gradient support for (`RadialGradient`). Thanks to [@wmedrano](https://github.com/wmedrano) +- Sweep gradient support (`SweepGradient`). + Thanks to [@wmedrano](https://github.com/wmedrano) ### Changed - The `RadialGradient::new` requires a start radius now. Set the second argument diff --git a/src/lib.rs b/src/lib.rs index 718b4ed..6660881 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,7 +63,7 @@ pub use mask::{Mask, MaskType}; pub use painter::{FillRule, Paint}; pub use pixmap::{Pixmap, PixmapMut, PixmapRef, BYTES_PER_PIXEL}; pub use shaders::{FilterQuality, GradientStop, PixmapPaint, SpreadMode}; -pub use shaders::{LinearGradient, Pattern, RadialGradient, Shader}; +pub use shaders::{LinearGradient, Pattern, RadialGradient, Shader, SweepGradient}; pub use tiny_skia_path::{IntRect, IntSize, NonZeroRect, Point, Rect, Size, Transform}; pub use tiny_skia_path::{LineCap, LineJoin, Stroke, StrokeDash}; diff --git a/src/pipeline/highp.rs b/src/pipeline/highp.rs index b0df476..214e31a 100644 --- a/src/pipeline/highp.rs +++ b/src/pipeline/highp.rs @@ -113,6 +113,7 @@ pub const STAGES: &[StageFn; super::STAGES_COUNT] = &[ repeat_x1, gradient, evenly_spaced_2_stop_gradient, + xy_to_unit_angle, xy_to_radius, xy_to_2pt_conical_focal_on_circle, xy_to_2pt_conical_well_behaved, @@ -988,6 +989,34 @@ fn evenly_spaced_2_stop_gradient(p: &mut Pipeline) { p.next_stage(); } +fn xy_to_unit_angle(p: &mut Pipeline) { + let x = p.r; + let y = p.g; + let x_abs = x.abs(); + let y_abs = y.abs(); + let slope = x_abs.min(y_abs) / x_abs.max(y_abs); + let s = slope * slope; + // Use a 7th degree polynomial to approximate atan. + // This was generated using sollya.gforge.inria.fr. + // A float optimized polynomial was generated using the following command. + // P1 = fpminimax((1/(2*Pi))*atan(x),[|1,3,5,7|],[|24...|],[2^(-40),1],relative); + let phi = slope + * (f32x8::splat(0.15912117063999176025390625) + + s * (f32x8::splat(-5.185396969318389892578125e-2) + + s * (f32x8::splat(2.476101927459239959716796875e-2) + + s * (f32x8::splat(-7.0547382347285747528076171875e-3))))); + let phi = x_abs.cmp_lt(y_abs).blend(f32x8::splat(0.25) - phi, phi); + let phi = x + .cmp_lt(f32x8::splat(0.0)) + .blend(f32x8::splat(0.5) - phi, phi); + let phi = y + .cmp_lt(f32x8::splat(0.0)) + .blend(f32x8::splat(1.0) - phi, phi); + let phi = phi.cmp_ne(phi).blend(f32x8::splat(0.0), phi); + p.r = phi; + p.next_stage(); +} + fn xy_to_radius(p: &mut Pipeline) { let x2 = p.r * p.r; let y2 = p.g * p.g; diff --git a/src/pipeline/lowp.rs b/src/pipeline/lowp.rs index e3869cc..fc1a6c5 100644 --- a/src/pipeline/lowp.rs +++ b/src/pipeline/lowp.rs @@ -126,6 +126,9 @@ pub const STAGES: &[StageFn; super::STAGES_COUNT] = &[ repeat_x1, gradient, evenly_spaced_2_stop_gradient, + // TODO: Can be implemented for lowp as well. The implementation is very similar to its highp + // variant. + null_fn, // XYToUnitAngle xy_to_radius, null_fn, // XYTo2PtConicalFocalOnCircle null_fn, // XYTo2PtConicalWellBehaved diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index 7e7e2ac..c4c0148 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -124,6 +124,7 @@ pub enum Stage { RepeatX1, Gradient, EvenlySpaced2StopGradient, + XYToUnitAngle, XYToRadius, XYTo2PtConicalFocalOnCircle, XYTo2PtConicalWellBehaved, diff --git a/src/shaders/mod.rs b/src/shaders/mod.rs index 27c452a..900ab68 100644 --- a/src/shaders/mod.rs +++ b/src/shaders/mod.rs @@ -8,6 +8,7 @@ mod gradient; mod linear_gradient; mod pattern; mod radial_gradient; +mod sweep_gradient; use tiny_skia_path::{NormalizedF32, Scalar}; @@ -15,6 +16,7 @@ pub use gradient::GradientStop; pub use linear_gradient::LinearGradient; pub use pattern::{FilterQuality, Pattern, PixmapPaint}; pub use radial_gradient::RadialGradient; +pub use sweep_gradient::SweepGradient; use crate::{Color, ColorSpace, Transform}; @@ -52,6 +54,8 @@ pub enum Shader<'a> { LinearGradient(LinearGradient), /// A radial gradient shader. RadialGradient(RadialGradient), + /// A sweep gradient shader. + SweepGradient(SweepGradient), /// A pattern shader. Pattern(Pattern<'a>), } @@ -60,9 +64,12 @@ impl Shader<'_> { /// Checks if the shader is guaranteed to produce only opaque colors. pub fn is_opaque(&self) -> bool { match self { - Shader::SolidColor(ref c) => c.is_opaque(), - Shader::LinearGradient(ref g) => g.is_opaque(), + Shader::SolidColor(c) => c.is_opaque(), + Shader::LinearGradient(g) => g.is_opaque(), + // A radial gradient may have points that are "undefined" so we just assume that it is + // not opaque. Shader::RadialGradient(_) => false, + Shader::SweepGradient(g) => g.is_opaque(), Shader::Pattern(_) => false, } } @@ -78,9 +85,10 @@ impl Shader<'_> { p.push_uniform_color(color); true } - Shader::LinearGradient(ref g) => g.push_stages(cs, p), - Shader::RadialGradient(ref g) => g.push_stages(cs, p), - Shader::Pattern(ref patt) => patt.push_stages(cs, p), + Shader::LinearGradient(g) => g.push_stages(cs, p), + Shader::RadialGradient(g) => g.push_stages(cs, p), + Shader::SweepGradient(g) => g.push_stages(cs, p), + Shader::Pattern(patt) => patt.push_stages(cs, p), } } @@ -94,6 +102,9 @@ impl Shader<'_> { Shader::RadialGradient(g) => { g.base.transform = g.base.transform.post_concat(ts); } + Shader::SweepGradient(g) => { + g.base.transform = g.base.transform.post_concat(ts); + } Shader::Pattern(p) => { p.transform = p.transform.post_concat(ts); } @@ -124,6 +135,9 @@ impl Shader<'_> { Shader::RadialGradient(g) => { g.base.apply_opacity(opacity); } + Shader::SweepGradient(g) => { + g.base.apply_opacity(opacity); + } Shader::Pattern(ref mut p) => { p.opacity = NormalizedF32::new(p.opacity.get() * opacity.bound(0.0, 1.0)).unwrap(); } diff --git a/src/shaders/sweep_gradient.rs b/src/shaders/sweep_gradient.rs new file mode 100644 index 0000000..d090aa4 --- /dev/null +++ b/src/shaders/sweep_gradient.rs @@ -0,0 +1,105 @@ +// Copyright 2006 The Android Open Source Project +// +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use alloc::vec::Vec; + +use tiny_skia_path::Scalar; + +use crate::{ColorSpace, GradientStop, Point, Shader, SpreadMode, Transform}; + +use super::gradient::{Gradient, DEGENERATE_THRESHOLD}; +use crate::pipeline::{RasterPipelineBuilder, Stage}; + +#[cfg(all(not(feature = "std"), feature = "no-std-float"))] +use tiny_skia_path::NoStdFloat; + +/// A radial gradient. +#[derive(Clone, PartialEq, Debug)] +pub struct SweepGradient { + pub(crate) base: Gradient, + t0: f32, + t1: f32, +} + +impl SweepGradient { + /// Creates a new 2-point conical gradient shader. + #[allow(clippy::new_ret_no_self)] + pub fn new( + center: Point, + start_angle: f32, + end_angle: f32, + stops: Vec, + mut mode: SpreadMode, + transform: Transform, + ) -> Option> { + if !start_angle.is_finite() || !end_angle.is_finite() || start_angle > end_angle { + return None; + } + + match stops.as_slice() { + [] => return None, + [stop] => return Some(Shader::SolidColor(stop.color)), + _ => (), + } + transform.invert()?; + if start_angle.is_nearly_equal_within_tolerance(end_angle, DEGENERATE_THRESHOLD) { + if mode == SpreadMode::Pad && end_angle > DEGENERATE_THRESHOLD { + // In this case, the first color is repeated from 0 to the angle, then a hardstop + // switches to the last color (all other colors are compressed to the infinitely + // thin interpolation region). + let front_color = stops.first().unwrap().color; + let back_color = stops.last().unwrap().color; + let mut new_stops = stops; + new_stops.clear(); + new_stops.extend_from_slice(&[ + GradientStop::new(0.0, front_color), + GradientStop::new(1.0, front_color), + GradientStop::new(1.0, back_color), + ]); + return SweepGradient::new(center, 0.0, end_angle, new_stops, mode, transform); + } + // TODO: Consider making a degenerate fallback shader similar to Skia. Tiny Skia + // currently opts to return `None` in some places. + return None; + } + if start_angle <= 0.0 && end_angle >= 360.0 { + mode = SpreadMode::Pad; + } + let t0 = start_angle / 360.0; + let t1 = end_angle / 360.0; + Some(Shader::SweepGradient(SweepGradient { + base: Gradient::new( + stops, + mode, + transform, + Transform::from_translate(-center.x, -center.y), + ), + t0, + t1, + })) + } + + pub(crate) fn is_opaque(&self) -> bool { + self.base.colors_are_opaque + } + + pub(crate) fn push_stages(&self, cs: ColorSpace, p: &mut RasterPipelineBuilder) -> bool { + let scale = 1.0 / (self.t1 - self.t0); + let bias = -scale * self.t0; + p.ctx.two_point_conical_gradient.p0 = scale; + p.ctx.two_point_conical_gradient.p1 = bias; + self.base.push_stages( + p, + cs, + &|p| { + p.push(Stage::XYToUnitAngle); + if scale != 1.0 && bias != 0.0 { + p.push(Stage::ApplyConcentricScaleBias) + } + }, + &|_| {}, + ) + } +} diff --git a/tests/images/gradients/sweep-gradient-full.png b/tests/images/gradients/sweep-gradient-full.png new file mode 100644 index 0000000..b9527f7 Binary files /dev/null and b/tests/images/gradients/sweep-gradient-full.png differ diff --git a/tests/images/gradients/sweep-gradient.png b/tests/images/gradients/sweep-gradient.png new file mode 100644 index 0000000..88d0607 Binary files /dev/null and b/tests/images/gradients/sweep-gradient.png differ diff --git a/tests/integration/gradients.rs b/tests/integration/gradients.rs index dfcf271..87d7af0 100644 --- a/tests/integration/gradients.rs +++ b/tests/integration/gradients.rs @@ -525,3 +525,67 @@ fn conical_smaller_radial() { let expected = Pixmap::load_png("tests/images/gradients/conical-smaller-radial.png").unwrap(); assert_eq!(pixmap, expected); } + +#[test] +fn sweep_gradient() { + let mut paint = Paint::default(); + paint.anti_alias = false; + paint.shader = SweepGradient::new( + Point::from_xy(100.0, 100.0), + 135.0, + 225.0, + vec![ + GradientStop::new(0.0, Color::from_rgba8(50, 127, 150, 200)), + GradientStop::new(1.0, Color::from_rgba8(220, 140, 75, 180)), + ], + SpreadMode::Pad, + Transform::identity(), + ) + .unwrap(); + + let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); + + let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path( + &path, + &paint, + FillRule::Winding, + Transform::identity(), + None, + ); + + let expected = Pixmap::load_png("tests/images/gradients/sweep-gradient.png").unwrap(); + assert_eq!(pixmap, expected); +} + +#[test] +fn sweep_gradient_full() { + let mut paint = Paint::default(); + paint.anti_alias = false; + paint.shader = SweepGradient::new( + Point::from_xy(100.0, 100.0), + 0.0, + 360.0, + vec![ + GradientStop::new(0.0, Color::from_rgba8(50, 127, 150, 200)), + GradientStop::new(1.0, Color::from_rgba8(220, 140, 75, 180)), + ], + SpreadMode::Pad, + Transform::identity(), + ) + .unwrap(); + + let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); + + let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path( + &path, + &paint, + FillRule::Winding, + Transform::identity(), + None, + ); + + let expected = Pixmap::load_png("tests/images/gradients/sweep-gradient-full.png").unwrap(); + assert_eq!(pixmap, expected); +}