Skip to content

Commit 2df5c25

Browse files
authored
Add support for sweep gradients (#166)
1 parent 6943c20 commit 2df5c25

File tree

10 files changed

+224
-6
lines changed

10 files changed

+224
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
88
### Added
99
- 2-point conical gradient support for (`RadialGradient`).
1010
Thanks to [@wmedrano](https://github.com/wmedrano)
11+
- Sweep gradient support (`SweepGradient`).
12+
Thanks to [@wmedrano](https://github.com/wmedrano)
1113

1214
### Changed
1315
- The `RadialGradient::new` requires a start radius now. Set the second argument

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub use mask::{Mask, MaskType};
6363
pub use painter::{FillRule, Paint};
6464
pub use pixmap::{Pixmap, PixmapMut, PixmapRef, BYTES_PER_PIXEL};
6565
pub use shaders::{FilterQuality, GradientStop, PixmapPaint, SpreadMode};
66-
pub use shaders::{LinearGradient, Pattern, RadialGradient, Shader};
66+
pub use shaders::{LinearGradient, Pattern, RadialGradient, Shader, SweepGradient};
6767

6868
pub use tiny_skia_path::{IntRect, IntSize, NonZeroRect, Point, Rect, Size, Transform};
6969
pub use tiny_skia_path::{LineCap, LineJoin, Stroke, StrokeDash};

src/pipeline/highp.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ pub const STAGES: &[StageFn; super::STAGES_COUNT] = &[
113113
repeat_x1,
114114
gradient,
115115
evenly_spaced_2_stop_gradient,
116+
xy_to_unit_angle,
116117
xy_to_radius,
117118
xy_to_2pt_conical_focal_on_circle,
118119
xy_to_2pt_conical_well_behaved,
@@ -988,6 +989,34 @@ fn evenly_spaced_2_stop_gradient(p: &mut Pipeline) {
988989
p.next_stage();
989990
}
990991

992+
fn xy_to_unit_angle(p: &mut Pipeline) {
993+
let x = p.r;
994+
let y = p.g;
995+
let x_abs = x.abs();
996+
let y_abs = y.abs();
997+
let slope = x_abs.min(y_abs) / x_abs.max(y_abs);
998+
let s = slope * slope;
999+
// Use a 7th degree polynomial to approximate atan.
1000+
// This was generated using sollya.gforge.inria.fr.
1001+
// A float optimized polynomial was generated using the following command.
1002+
// P1 = fpminimax((1/(2*Pi))*atan(x),[|1,3,5,7|],[|24...|],[2^(-40),1],relative);
1003+
let phi = slope
1004+
* (f32x8::splat(0.15912117063999176025390625)
1005+
+ s * (f32x8::splat(-5.185396969318389892578125e-2)
1006+
+ s * (f32x8::splat(2.476101927459239959716796875e-2)
1007+
+ s * (f32x8::splat(-7.0547382347285747528076171875e-3)))));
1008+
let phi = x_abs.cmp_lt(y_abs).blend(f32x8::splat(0.25) - phi, phi);
1009+
let phi = x
1010+
.cmp_lt(f32x8::splat(0.0))
1011+
.blend(f32x8::splat(0.5) - phi, phi);
1012+
let phi = y
1013+
.cmp_lt(f32x8::splat(0.0))
1014+
.blend(f32x8::splat(1.0) - phi, phi);
1015+
let phi = phi.cmp_ne(phi).blend(f32x8::splat(0.0), phi);
1016+
p.r = phi;
1017+
p.next_stage();
1018+
}
1019+
9911020
fn xy_to_radius(p: &mut Pipeline) {
9921021
let x2 = p.r * p.r;
9931022
let y2 = p.g * p.g;

src/pipeline/lowp.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ pub const STAGES: &[StageFn; super::STAGES_COUNT] = &[
126126
repeat_x1,
127127
gradient,
128128
evenly_spaced_2_stop_gradient,
129+
// TODO: Can be implemented for lowp as well. The implementation is very similar to its highp
130+
// variant.
131+
null_fn, // XYToUnitAngle
129132
xy_to_radius,
130133
null_fn, // XYTo2PtConicalFocalOnCircle
131134
null_fn, // XYTo2PtConicalWellBehaved

src/pipeline/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ pub enum Stage {
124124
RepeatX1,
125125
Gradient,
126126
EvenlySpaced2StopGradient,
127+
XYToUnitAngle,
127128
XYToRadius,
128129
XYTo2PtConicalFocalOnCircle,
129130
XYTo2PtConicalWellBehaved,

src/shaders/mod.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ mod gradient;
88
mod linear_gradient;
99
mod pattern;
1010
mod radial_gradient;
11+
mod sweep_gradient;
1112

1213
use tiny_skia_path::{NormalizedF32, Scalar};
1314

1415
pub use gradient::GradientStop;
1516
pub use linear_gradient::LinearGradient;
1617
pub use pattern::{FilterQuality, Pattern, PixmapPaint};
1718
pub use radial_gradient::RadialGradient;
19+
pub use sweep_gradient::SweepGradient;
1820

1921
use crate::{Color, ColorSpace, Transform};
2022

@@ -52,6 +54,8 @@ pub enum Shader<'a> {
5254
LinearGradient(LinearGradient),
5355
/// A radial gradient shader.
5456
RadialGradient(RadialGradient),
57+
/// A sweep gradient shader.
58+
SweepGradient(SweepGradient),
5559
/// A pattern shader.
5660
Pattern(Pattern<'a>),
5761
}
@@ -60,9 +64,12 @@ impl Shader<'_> {
6064
/// Checks if the shader is guaranteed to produce only opaque colors.
6165
pub fn is_opaque(&self) -> bool {
6266
match self {
63-
Shader::SolidColor(ref c) => c.is_opaque(),
64-
Shader::LinearGradient(ref g) => g.is_opaque(),
67+
Shader::SolidColor(c) => c.is_opaque(),
68+
Shader::LinearGradient(g) => g.is_opaque(),
69+
// A radial gradient may have points that are "undefined" so we just assume that it is
70+
// not opaque.
6571
Shader::RadialGradient(_) => false,
72+
Shader::SweepGradient(g) => g.is_opaque(),
6673
Shader::Pattern(_) => false,
6774
}
6875
}
@@ -78,9 +85,10 @@ impl Shader<'_> {
7885
p.push_uniform_color(color);
7986
true
8087
}
81-
Shader::LinearGradient(ref g) => g.push_stages(cs, p),
82-
Shader::RadialGradient(ref g) => g.push_stages(cs, p),
83-
Shader::Pattern(ref patt) => patt.push_stages(cs, p),
88+
Shader::LinearGradient(g) => g.push_stages(cs, p),
89+
Shader::RadialGradient(g) => g.push_stages(cs, p),
90+
Shader::SweepGradient(g) => g.push_stages(cs, p),
91+
Shader::Pattern(patt) => patt.push_stages(cs, p),
8492
}
8593
}
8694

@@ -94,6 +102,9 @@ impl Shader<'_> {
94102
Shader::RadialGradient(g) => {
95103
g.base.transform = g.base.transform.post_concat(ts);
96104
}
105+
Shader::SweepGradient(g) => {
106+
g.base.transform = g.base.transform.post_concat(ts);
107+
}
97108
Shader::Pattern(p) => {
98109
p.transform = p.transform.post_concat(ts);
99110
}
@@ -124,6 +135,9 @@ impl Shader<'_> {
124135
Shader::RadialGradient(g) => {
125136
g.base.apply_opacity(opacity);
126137
}
138+
Shader::SweepGradient(g) => {
139+
g.base.apply_opacity(opacity);
140+
}
127141
Shader::Pattern(ref mut p) => {
128142
p.opacity = NormalizedF32::new(p.opacity.get() * opacity.bound(0.0, 1.0)).unwrap();
129143
}

src/shaders/sweep_gradient.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2006 The Android Open Source Project
2+
//
3+
// Use of this source code is governed by a BSD-style license that can be
4+
// found in the LICENSE file.
5+
6+
use alloc::vec::Vec;
7+
8+
use tiny_skia_path::Scalar;
9+
10+
use crate::{ColorSpace, GradientStop, Point, Shader, SpreadMode, Transform};
11+
12+
use super::gradient::{Gradient, DEGENERATE_THRESHOLD};
13+
use crate::pipeline::{RasterPipelineBuilder, Stage};
14+
15+
#[cfg(all(not(feature = "std"), feature = "no-std-float"))]
16+
use tiny_skia_path::NoStdFloat;
17+
18+
/// A radial gradient.
19+
#[derive(Clone, PartialEq, Debug)]
20+
pub struct SweepGradient {
21+
pub(crate) base: Gradient,
22+
t0: f32,
23+
t1: f32,
24+
}
25+
26+
impl SweepGradient {
27+
/// Creates a new 2-point conical gradient shader.
28+
#[allow(clippy::new_ret_no_self)]
29+
pub fn new(
30+
center: Point,
31+
start_angle: f32,
32+
end_angle: f32,
33+
stops: Vec<GradientStop>,
34+
mut mode: SpreadMode,
35+
transform: Transform,
36+
) -> Option<Shader<'static>> {
37+
if !start_angle.is_finite() || !end_angle.is_finite() || start_angle > end_angle {
38+
return None;
39+
}
40+
41+
match stops.as_slice() {
42+
[] => return None,
43+
[stop] => return Some(Shader::SolidColor(stop.color)),
44+
_ => (),
45+
}
46+
transform.invert()?;
47+
if start_angle.is_nearly_equal_within_tolerance(end_angle, DEGENERATE_THRESHOLD) {
48+
if mode == SpreadMode::Pad && end_angle > DEGENERATE_THRESHOLD {
49+
// In this case, the first color is repeated from 0 to the angle, then a hardstop
50+
// switches to the last color (all other colors are compressed to the infinitely
51+
// thin interpolation region).
52+
let front_color = stops.first().unwrap().color;
53+
let back_color = stops.last().unwrap().color;
54+
let mut new_stops = stops;
55+
new_stops.clear();
56+
new_stops.extend_from_slice(&[
57+
GradientStop::new(0.0, front_color),
58+
GradientStop::new(1.0, front_color),
59+
GradientStop::new(1.0, back_color),
60+
]);
61+
return SweepGradient::new(center, 0.0, end_angle, new_stops, mode, transform);
62+
}
63+
// TODO: Consider making a degenerate fallback shader similar to Skia. Tiny Skia
64+
// currently opts to return `None` in some places.
65+
return None;
66+
}
67+
if start_angle <= 0.0 && end_angle >= 360.0 {
68+
mode = SpreadMode::Pad;
69+
}
70+
let t0 = start_angle / 360.0;
71+
let t1 = end_angle / 360.0;
72+
Some(Shader::SweepGradient(SweepGradient {
73+
base: Gradient::new(
74+
stops,
75+
mode,
76+
transform,
77+
Transform::from_translate(-center.x, -center.y),
78+
),
79+
t0,
80+
t1,
81+
}))
82+
}
83+
84+
pub(crate) fn is_opaque(&self) -> bool {
85+
self.base.colors_are_opaque
86+
}
87+
88+
pub(crate) fn push_stages(&self, cs: ColorSpace, p: &mut RasterPipelineBuilder) -> bool {
89+
let scale = 1.0 / (self.t1 - self.t0);
90+
let bias = -scale * self.t0;
91+
p.ctx.two_point_conical_gradient.p0 = scale;
92+
p.ctx.two_point_conical_gradient.p1 = bias;
93+
self.base.push_stages(
94+
p,
95+
cs,
96+
&|p| {
97+
p.push(Stage::XYToUnitAngle);
98+
if scale != 1.0 && bias != 0.0 {
99+
p.push(Stage::ApplyConcentricScaleBias)
100+
}
101+
},
102+
&|_| {},
103+
)
104+
}
105+
}
9.71 KB
Loading
6.03 KB
Loading

tests/integration/gradients.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,3 +525,67 @@ fn conical_smaller_radial() {
525525
let expected = Pixmap::load_png("tests/images/gradients/conical-smaller-radial.png").unwrap();
526526
assert_eq!(pixmap, expected);
527527
}
528+
529+
#[test]
530+
fn sweep_gradient() {
531+
let mut paint = Paint::default();
532+
paint.anti_alias = false;
533+
paint.shader = SweepGradient::new(
534+
Point::from_xy(100.0, 100.0),
535+
135.0,
536+
225.0,
537+
vec![
538+
GradientStop::new(0.0, Color::from_rgba8(50, 127, 150, 200)),
539+
GradientStop::new(1.0, Color::from_rgba8(220, 140, 75, 180)),
540+
],
541+
SpreadMode::Pad,
542+
Transform::identity(),
543+
)
544+
.unwrap();
545+
546+
let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap());
547+
548+
let mut pixmap = Pixmap::new(200, 200).unwrap();
549+
pixmap.fill_path(
550+
&path,
551+
&paint,
552+
FillRule::Winding,
553+
Transform::identity(),
554+
None,
555+
);
556+
557+
let expected = Pixmap::load_png("tests/images/gradients/sweep-gradient.png").unwrap();
558+
assert_eq!(pixmap, expected);
559+
}
560+
561+
#[test]
562+
fn sweep_gradient_full() {
563+
let mut paint = Paint::default();
564+
paint.anti_alias = false;
565+
paint.shader = SweepGradient::new(
566+
Point::from_xy(100.0, 100.0),
567+
0.0,
568+
360.0,
569+
vec![
570+
GradientStop::new(0.0, Color::from_rgba8(50, 127, 150, 200)),
571+
GradientStop::new(1.0, Color::from_rgba8(220, 140, 75, 180)),
572+
],
573+
SpreadMode::Pad,
574+
Transform::identity(),
575+
)
576+
.unwrap();
577+
578+
let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap());
579+
580+
let mut pixmap = Pixmap::new(200, 200).unwrap();
581+
pixmap.fill_path(
582+
&path,
583+
&paint,
584+
FillRule::Winding,
585+
Transform::identity(),
586+
None,
587+
);
588+
589+
let expected = Pixmap::load_png("tests/images/gradients/sweep-gradient-full.png").unwrap();
590+
assert_eq!(pixmap, expected);
591+
}

0 commit comments

Comments
 (0)