Skip to content

Commit

Permalink
Sweep gradients
Browse files Browse the repository at this point in the history
Implements support for rendering sweep gradients. Based on Skia's SkSweepGradient shader.
  • Loading branch information
dfrg committed Feb 13, 2024
1 parent d16df77 commit e6b5ae9
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 36 deletions.
17 changes: 17 additions & 0 deletions crates/encoding/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ impl DrawTag {
/// Radial gradient fill.
pub const RADIAL_GRADIENT: Self = Self(0x29c);

/// Sweep gradient fill.
pub const SWEEP_GRADIENT: Self = Self(0x254);

/// Image fill.
pub const IMAGE: Self = Self(0x248);

Expand Down Expand Up @@ -99,6 +102,20 @@ pub struct DrawRadialGradient {
pub r1: f32,
}

/// Draw data for a sweep gradient.
#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
#[repr(C)]
pub struct DrawSweepGradient {
/// Ramp index.
pub index: u32,
/// Center point.
pub p0: [f32; 2],
/// Normalized start angle.
pub t0: f32,
/// Normalized end angle.
pub t1: f32,
}

/// Draw data for an image.
#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
#[repr(C)]
Expand Down
50 changes: 47 additions & 3 deletions crates/encoding/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ use peniko::{

#[cfg(feature = "full")]
use {
super::{DrawImage, DrawLinearGradient, DrawRadialGradient, Glyph, GlyphRun, Patch},
super::{
DrawImage, DrawLinearGradient, DrawRadialGradient, DrawSweepGradient, Glyph, GlyphRun,
Patch,
},
peniko::{ColorStop, Extend, GradientKind, Image},
skrifa::instance::NormalizedCoord,
};
Expand Down Expand Up @@ -294,8 +297,23 @@ impl Encoding {
gradient.extend,
);
}
GradientKind::Sweep { .. } => {
todo!("sweep gradients aren't supported yet!")
GradientKind::Sweep {
center,
start_angle,
end_angle,
} => {
use core::f32::consts::TAU;
self.encode_sweep_gradient(
DrawSweepGradient {
index: 0,
p0: point_to_f32(center),
t0: start_angle / TAU,
t1: end_angle / TAU,
},
gradient.stops.iter().copied(),
alpha,
gradient.extend,
);
}
},
#[cfg(feature = "full")]
Expand Down Expand Up @@ -347,6 +365,7 @@ impl Encoding {
const SKIA_EPSILON: f32 = 1.0 / (1 << 12) as f32;
if gradient.p0 == gradient.p1 && (gradient.r0 - gradient.r1).abs() < SKIA_EPSILON {
self.encode_color(DrawColor::new(Color::TRANSPARENT));
return;
}
match self.add_ramp(color_stops, alpha, extend) {
RampStops::Empty => self.encode_color(DrawColor::new(Color::TRANSPARENT)),
Expand All @@ -359,6 +378,31 @@ impl Encoding {
}
}

/// Encodes a radial gradient brush.
#[cfg(feature = "full")]
pub fn encode_sweep_gradient(
&mut self,
gradient: DrawSweepGradient,
color_stops: impl Iterator<Item = ColorStop>,
alpha: f32,
extend: Extend,
) {
const SKIA_DEGENERATE_THRESHOLD: f32 = 1.0 / (1 << 15) as f32;
if (gradient.t0 - gradient.t1).abs() < SKIA_DEGENERATE_THRESHOLD {
self.encode_color(DrawColor::new(Color::TRANSPARENT));
return;
}
match self.add_ramp(color_stops, alpha, extend) {
RampStops::Empty => self.encode_color(DrawColor::new(Color::TRANSPARENT)),
RampStops::One(color) => self.encode_color(DrawColor::new(color)),
_ => {
self.draw_tags.push(DrawTag::SWEEP_GRADIENT);
self.draw_data
.extend_from_slice(bytemuck::bytes_of(&gradient));
}
}
}

/// Encodes an image brush.
#[cfg(feature = "full")]
pub fn encode_image(&mut self, image: &Image, _alpha: f32) {
Expand Down
2 changes: 1 addition & 1 deletion crates/encoding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub use config::{
};
pub use draw::{
DrawBbox, DrawBeginClip, DrawColor, DrawImage, DrawLinearGradient, DrawMonoid,
DrawRadialGradient, DrawTag, DRAW_INFO_FLAGS_FILL_RULE_BIT,
DrawRadialGradient, DrawSweepGradient, DrawTag, DRAW_INFO_FLAGS_FILL_RULE_BIT,
};
pub use encoding::{Encoding, StreamOffsets};
pub use mask::{make_mask_lut, make_mask_lut_16};
Expand Down
50 changes: 34 additions & 16 deletions examples/scenes/src/test_scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,22 +656,38 @@ fn brush_transform(scene: &mut Scene, params: &mut SceneParams) {
}

fn gradient_extend(scene: &mut Scene, params: &mut SceneParams) {
fn square(scene: &mut Scene, is_radial: bool, transform: Affine, extend: Extend) {
enum Kind {
Linear,
Radial,
Sweep,
}
fn square(scene: &mut Scene, kind: Kind, transform: Affine, extend: Extend) {
let colors = [Color::RED, Color::rgb8(0, 255, 0), Color::BLUE];
let width = 300f64;
let height = 300f64;
let gradient: Brush = if is_radial {
let center = (width * 0.5, height * 0.5);
let radius = (width * 0.25) as f32;
Gradient::new_two_point_radial(center, radius * 0.25, center, radius)
.with_stops(colors)
.with_extend(extend)
.into()
} else {
Gradient::new_linear((width * 0.35, height * 0.5), (width * 0.65, height * 0.5))
.with_stops(colors)
.with_extend(extend)
.into()
let gradient: Brush = match kind {
Kind::Linear => {
Gradient::new_linear((width * 0.35, height * 0.5), (width * 0.65, height * 0.5))
.with_stops(colors)
.with_extend(extend)
.into()
}
Kind::Radial => {
let center = (width * 0.5, height * 0.5);
let radius = (width * 0.25) as f32;
Gradient::new_two_point_radial(center, radius * 0.25, center, radius)
.with_stops(colors)
.with_extend(extend)
.into()
}
Kind::Sweep => Gradient::new_sweep(
(width * 0.5, height * 0.5),
30f32.to_radians(),
150f32.to_radians(),
)
.with_stops(colors)
.with_extend(extend)
.into(),
};
scene.fill(
Fill::NonZero,
Expand All @@ -683,10 +699,12 @@ fn gradient_extend(scene: &mut Scene, params: &mut SceneParams) {
}
let extend_modes = [Extend::Pad, Extend::Repeat, Extend::Reflect];
for (x, extend) in extend_modes.iter().enumerate() {
for y in 0..2 {
let is_radial = y & 1 != 0;
for (y, kind) in [Kind::Linear, Kind::Radial, Kind::Sweep]
.into_iter()
.enumerate()
{
let transform = Affine::translate((x as f64 * 350.0 + 50.0, y as f64 * 350.0 + 100.0));
square(scene, is_radial, transform, *extend);
square(scene, kind, transform, *extend);
}
}
for (i, label) in ["Pad", "Repeat", "Reflect"].iter().enumerate() {
Expand Down
7 changes: 7 additions & 0 deletions shader/coarse.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,13 @@ fn main(
let info_offset = di + 1u;
write_grad(CMD_RAD_GRAD, index, info_offset);
}
// DRAWTAG_FILL_SWEEP_GRADIENT
case 0x254u: {
write_path(tile, tile_ix, draw_flags);
let index = scene[dd];
let info_offset = di + 1u;
write_grad(CMD_SWEEP_GRAD, index, info_offset);
}
// DRAWTAG_FILL_IMAGE
case 0x248u: {
write_path(tile, tile_ix, draw_flags);
Expand Down
21 changes: 18 additions & 3 deletions shader/draw_leaf.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ fn main(
let dd = config.drawdata_base + m.scene_offset;
let di = m.info_offset;
if tag_word == DRAWTAG_FILL_COLOR || tag_word == DRAWTAG_FILL_LIN_GRADIENT ||
tag_word == DRAWTAG_FILL_RAD_GRADIENT || tag_word == DRAWTAG_FILL_IMAGE ||
tag_word == DRAWTAG_BEGIN_CLIP
tag_word == DRAWTAG_FILL_RAD_GRADIENT || tag_word == DRAWTAG_FILL_SWEEP_GRADIENT ||
tag_word == DRAWTAG_FILL_IMAGE || tag_word == DRAWTAG_BEGIN_CLIP
{
let bbox = path_bbox[m.path_ix];
// TODO: bbox is mostly yagni here, sort that out. Maybe clips?
Expand All @@ -111,7 +111,7 @@ fn main(
var transform = Transform();
let draw_flags = bbox.draw_flags;
if tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT ||
tag_word == DRAWTAG_FILL_IMAGE
tag_word == DRAWTAG_FILL_SWEEP_GRADIENT || tag_word == DRAWTAG_FILL_IMAGE
{
transform = read_transform(config.transform_base, bbox.trans_ix);
}
Expand Down Expand Up @@ -219,6 +219,21 @@ fn main(
info[di + 8u] = bitcast<u32>(radius);
info[di + 9u] = bitcast<u32>((flags << 3u) | kind);
}
// DRAWTAG_FILL_SWEEP_GRADIENT
case 0x254u: {
info[di] = draw_flags;
let p0 = bitcast<vec2<f32>>(vec2(scene[dd + 1u], scene[dd + 2u]));
let xform = transform_mul(transform, Transform(vec4(1.0, 0.0, 0.0, 1.0), p0));
let inv = transform_inverse(xform);
info[di + 1u] = bitcast<u32>(inv.matrx.x);
info[di + 2u] = bitcast<u32>(inv.matrx.y);
info[di + 3u] = bitcast<u32>(inv.matrx.z);
info[di + 4u] = bitcast<u32>(inv.matrx.w);
info[di + 5u] = bitcast<u32>(inv.translate.x);
info[di + 6u] = bitcast<u32>(inv.translate.y);
info[di + 7u] = scene[dd + 3u];
info[di + 8u] = scene[dd + 4u];
}
// DRAWTAG_FILL_IMAGE
case 0x248u: {
info[di] = draw_flags;
Expand Down
58 changes: 54 additions & 4 deletions shader/fine.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,22 @@ fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad {
return CmdRadGrad(index, extend_mode, matrx, xlat, focal_x, radius, kind, flags);
}

fn read_sweep_grad(cmd_ix: u32) -> CmdSweepGrad {
let index_mode = ptcl[cmd_ix + 1u];
let index = index_mode >> 2u;
let extend_mode = index_mode & 0x3u;
let info_offset = ptcl[cmd_ix + 2u];
let m0 = bitcast<f32>(info[info_offset]);
let m1 = bitcast<f32>(info[info_offset + 1u]);
let m2 = bitcast<f32>(info[info_offset + 2u]);
let m3 = bitcast<f32>(info[info_offset + 3u]);
let matrx = vec4(m0, m1, m2, m3);
let xlat = vec2(bitcast<f32>(info[info_offset + 4u]), bitcast<f32>(info[info_offset + 5u]));
let t0 = bitcast<f32>(info[info_offset + 6u]);
let t1 = bitcast<f32>(info[info_offset + 7u]);
return CmdSweepGrad(index, extend_mode, matrx, xlat, t0, t1);
}

fn read_image(cmd_ix: u32) -> CmdImage {
let info_offset = ptcl[cmd_ix + 1u];
let m0 = bitcast<f32>(info[info_offset]);
Expand Down Expand Up @@ -970,8 +986,42 @@ fn main(
}
cmd_ix += 3u;
}
// CMD_IMAGE
// CMD_SWEEP_GRAD
case 8u: {
let sweep = read_sweep_grad(cmd_ix);
let scale = 1.0 / (sweep.t1 - sweep.t0);
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
let my_xy = vec2(xy.x + f32(i), xy.y);
let local_xy = sweep.matrx.xy * my_xy.x + sweep.matrx.zw * my_xy.y + sweep.xlat;
let x = local_xy.x;
let y = local_xy.y;
// xy_to_unit_angle from Skia:
// See <https://github.com/google/skia/blob/30bba741989865c157c7a997a0caebe94921276b/src/opts/SkRasterPipeline_opts.h#L5859>
let xabs = abs(x);
let yabs = abs(y);
let slope = min(xabs, yabs) / max(xabs, yabs);
let s = slope * slope;
// again, from Skia:
// 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);
var phi = slope * (0.15912117063999176025390625f + s * (-5.185396969318389892578125e-2f + s * (2.476101927459239959716796875e-2f + s * (-7.0547382347285747528076171875e-3f))));
phi = select(phi, 1.0 / 4.0 - phi, xabs < yabs);
phi = select(phi, 1.0 / 2.0 - phi, x < 0.0);
phi = select(phi, 1.0 - phi, y < 0.0);
phi = select(phi, 0.0, phi != phi); // check for NaN
phi = (phi - sweep.t0) * scale;
let t = extend_mode(phi, sweep.extend_mode);
let ramp_x = i32(round(t * f32(GRADIENT_WIDTH - 1)));
let fg_rgba = textureLoad(gradients, vec2(ramp_x, i32(sweep.index)), 0);
let fg_i = fg_rgba * area[i];
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
}
cmd_ix += 3u;
}
// CMD_IMAGE
case 9u: {
let image = read_image(cmd_ix);
let atlas_extents = image.atlas_offset + image.extents;
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
Expand All @@ -993,7 +1043,7 @@ fn main(
cmd_ix += 2u;
}
// CMD_BEGIN_CLIP
case 9u: {
case 10u: {
if clip_depth < BLEND_STACK_SPLIT {
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
blend_stack[clip_depth][i] = pack4x8unorm(rgba[i]);
Expand All @@ -1006,7 +1056,7 @@ fn main(
cmd_ix += 1u;
}
// CMD_END_CLIP
case 10u: {
case 11u: {
let end_clip = read_end_clip(cmd_ix);
clip_depth -= 1u;
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
Expand All @@ -1023,7 +1073,7 @@ fn main(
cmd_ix += 3u;
}
// CMD_JUMP
case 11u: {
case 12u: {
cmd_ix = ptcl[cmd_ix + 1u];
}
default: {}
Expand Down
1 change: 1 addition & 0 deletions shader/shared/drawtag.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let DRAWTAG_NOP = 0u;
let DRAWTAG_FILL_COLOR = 0x44u;
let DRAWTAG_FILL_LIN_GRADIENT = 0x114u;
let DRAWTAG_FILL_RAD_GRADIENT = 0x29cu;
let DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u;
let DRAWTAG_FILL_IMAGE = 0x248u;
let DRAWTAG_BEGIN_CLIP = 0x9u;
let DRAWTAG_END_CLIP = 0x21u;
Expand Down
18 changes: 14 additions & 4 deletions shader/shared/ptcl.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ let CMD_SOLID = 3u;
let CMD_COLOR = 5u;
let CMD_LIN_GRAD = 6u;
let CMD_RAD_GRAD = 7u;
let CMD_IMAGE = 8u;
let CMD_BEGIN_CLIP = 9u;
let CMD_END_CLIP = 10u;
let CMD_JUMP = 11u;
let CMD_SWEEP_GRAD = 8u;
let CMD_IMAGE = 9u;
let CMD_BEGIN_CLIP = 10u;
let CMD_END_CLIP = 11u;
let CMD_JUMP = 12u;

// The individual PTCL structs are written here, but read/write is by
// hand in the relevant shaders
Expand Down Expand Up @@ -62,6 +63,15 @@ struct CmdRadGrad {
flags: u32,
}

struct CmdSweepGrad {
index: u32,
extend_mode: u32,
matrx: vec4<f32>,
xlat: vec2<f32>,
t0: f32,
t1: f32,
}

struct CmdImage {
matrx: vec4<f32>,
xlat: vec2<f32>,
Expand Down
Loading

0 comments on commit e6b5ae9

Please sign in to comment.