Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
29 changes: 29 additions & 0 deletions src/pipeline/highp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/pipeline/lowp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/pipeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ pub enum Stage {
RepeatX1,
Gradient,
EvenlySpaced2StopGradient,
XYToUnitAngle,
XYToRadius,
XYTo2PtConicalFocalOnCircle,
XYTo2PtConicalWellBehaved,
Expand Down
24 changes: 19 additions & 5 deletions src/shaders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ mod gradient;
mod linear_gradient;
mod pattern;
mod radial_gradient;
mod sweep_gradient;

use tiny_skia_path::{NormalizedF32, Scalar};

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};

Expand Down Expand Up @@ -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>),
}
Expand All @@ -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,
}
}
Expand All @@ -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),
}
}

Expand All @@ -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);
}
Expand Down Expand Up @@ -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();
}
Expand Down
105 changes: 105 additions & 0 deletions src/shaders/sweep_gradient.rs
Original file line number Diff line number Diff line change
@@ -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<GradientStop>,
mut mode: SpreadMode,
transform: Transform,
) -> Option<Shader<'static>> {
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)
}
},
&|_| {},
)
}
}
Binary file added tests/images/gradients/sweep-gradient-full.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/gradients/sweep-gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions tests/integration/gradients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}