Extend modes for gradients

This patch implements the pad, repeat and reflect extend modes for gradient brushes. Adds a new example demonstrating the functionality.

Also fixes a few bugs:
* Clamps alpha in blend.wgsl for the `blend_compose` function. The `Plus` mode was generating `alpha > 1.0` leading to incorrect rendering.
* Small change to radial gradients in fine.wgsl to reject pixels outside the cone when the circles don't nest. This requires further work to properly extend the cone when one of the radii is not 0.
This commit is contained in:
Chad Brokaw 2023-04-30 23:11:57 -04:00
parent 299b47ea06
commit 15cd306af6
6 changed files with 133 additions and 19 deletions

View file

@ -113,11 +113,13 @@ impl Encoding {
Patch::Ramp {
draw_data_offset: offset,
stops,
extend,
} => {
let stops = stops.start + stops_base..stops.end + stops_base;
Patch::Ramp {
draw_data_offset: offset + offsets.draw_data,
stops,
extend: *extend,
}
}
Patch::GlyphRun { index } => Patch::GlyphRun {
@ -264,9 +266,9 @@ impl Encoding {
gradient: DrawLinearGradient,
color_stops: impl Iterator<Item = ColorStop>,
alpha: f32,
_extend: Extend,
extend: Extend,
) {
self.add_ramp(color_stops, alpha);
self.add_ramp(color_stops, alpha, extend);
self.draw_tags.push(DrawTag::LINEAR_GRADIENT);
self.draw_data
.extend_from_slice(bytemuck::bytes_of(&gradient));
@ -278,9 +280,9 @@ impl Encoding {
gradient: DrawRadialGradient,
color_stops: impl Iterator<Item = ColorStop>,
alpha: f32,
_extend: Extend,
extend: Extend,
) {
self.add_ramp(color_stops, alpha);
self.add_ramp(color_stops, alpha, extend);
self.draw_tags.push(DrawTag::RADIAL_GRADIENT);
self.draw_data
.extend_from_slice(bytemuck::bytes_of(&gradient));
@ -331,7 +333,12 @@ impl Encoding {
self.path_tags.swap(len - 1, len - 2);
}
fn add_ramp(&mut self, color_stops: impl Iterator<Item = ColorStop>, alpha: f32) {
fn add_ramp(
&mut self,
color_stops: impl Iterator<Item = ColorStop>,
alpha: f32,
extend: Extend,
) {
let offset = self.draw_data.len();
let stops_start = self.color_stops.len();
if alpha != 1.0 {
@ -343,6 +350,7 @@ impl Encoding {
self.patches.push(Patch::Ramp {
draw_data_offset: offset,
stops: stops_start..self.color_stops.len(),
extend,
});
}
}

View file

@ -4,7 +4,7 @@
use std::ops::Range;
use bytemuck::{Pod, Zeroable};
use peniko::Image;
use peniko::{Extend, Image};
use super::{
glyph_cache::{CachedRange, GlyphCache, GlyphKey},
@ -221,11 +221,13 @@ impl Resolver {
ResolvedPatch::Ramp {
draw_data_offset,
ramp_id,
extend,
} => {
if pos < *draw_data_offset {
data.extend_from_slice(&encoding.draw_data[pos..*draw_data_offset]);
}
data.extend_from_slice(bytemuck::bytes_of(ramp_id));
let index_mode = (ramp_id << 2) | *extend as u32;
data.extend_from_slice(bytemuck::bytes_of(&index_mode));
pos = *draw_data_offset + 4;
}
ResolvedPatch::GlyphRun { .. } => {}
@ -340,11 +342,13 @@ impl Resolver {
Patch::Ramp {
draw_data_offset,
stops,
extend,
} => {
let ramp_id = self.ramp_cache.add(&encoding.color_stops[stops.clone()]);
self.patches.push(ResolvedPatch::Ramp {
draw_data_offset: *draw_data_offset + sizes.draw_data,
ramp_id,
extend: *extend,
});
}
Patch::GlyphRun { index } => {
@ -472,6 +476,8 @@ pub enum Patch {
draw_data_offset: usize,
/// Range of the gradient stops in the resource set.
stops: Range<usize>,
/// Extend mode for the gradient.
extend: Extend,
},
/// Glyph run resource.
GlyphRun {
@ -501,6 +507,8 @@ enum ResolvedPatch {
draw_data_offset: usize,
/// Resolved ramp index.
ramp_id: u32,
/// Extend mode for the gradient.
extend: Extend,
},
GlyphRun {
/// Index of the original glyph run in the encoding.

View file

@ -30,6 +30,7 @@ pub fn test_scenes() -> SceneSet {
scene!(funky_paths),
scene!(cardioid_and_friends),
scene!(animated_text: animated),
scene!(gradient_extend),
scene!(brush_transform: animated),
scene!(blend_grid),
scene!(conflation_artifacts),
@ -249,6 +250,72 @@ fn brush_transform(sb: &mut SceneBuilder, params: &mut SceneParams) {
);
}
fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) {
fn square(
sb: &mut SceneBuilder,
is_radial: bool,
transform: Affine,
brush_transform: Option<Affine>,
extend: Extend,
) {
let colors = [Color::RED, Color::GREEN, Color::BLUE];
let width = 400f64;
let height = 400f64;
let gradient: Brush = if is_radial {
Gradient::new_radial((width * 0.5, height * 0.5), (width * 0.25) as f32)
.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()
};
sb.fill(
Fill::NonZero,
transform,
&gradient,
brush_transform,
&Rect::new(0.0, 0.0, width, height),
);
}
let extend_modes = [Extend::Pad, Extend::Repeat, Extend::Reflect];
for x in 0..3 {
let extend = extend_modes[x];
for y in 0..2 {
let is_radial = y & 1 != 0;
let transform = Affine::translate((x as f64 * 450.0 + 50.0, y as f64 * 450.0 + 100.0));
square(sb, is_radial, transform, None, extend);
}
}
let white = Color::WHITE.into();
params.text.add(
sb,
None,
32.0,
Some(&white),
Affine::translate((50.0, 70.0)),
"Pad",
);
params.text.add(
sb,
None,
32.0,
Some(&white),
Affine::translate((500.0, 70.0)),
"Repeat",
);
params.text.add(
sb,
None,
32.0,
Some(&white),
Affine::translate((950.0, 70.0)),
"Reflect",
);
}
fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) {
const BLEND_MODES: &[Mix] = &[
Mix::Normal,

View file

@ -61,16 +61,20 @@ fn read_color(cmd_ix: u32) -> CmdColor {
}
fn read_lin_grad(cmd_ix: u32) -> CmdLinGrad {
let index = ptcl[cmd_ix + 1u];
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 line_x = bitcast<f32>(info[info_offset]);
let line_y = bitcast<f32>(info[info_offset + 1u]);
let line_c = bitcast<f32>(info[info_offset + 2u]);
return CmdLinGrad(index, line_x, line_y, line_c);
return CmdLinGrad(index, extend_mode, line_x, line_y, line_c);
}
fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad {
let index = ptcl[cmd_ix + 1u];
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]);
@ -81,7 +85,7 @@ fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad {
let c1 = vec2(bitcast<f32>(info[info_offset + 6u]), bitcast<f32>(info[info_offset + 7u]));
let ra = bitcast<f32>(info[info_offset + 8u]);
let roff = bitcast<f32>(info[info_offset + 9u]);
return CmdRadGrad(index, matrx, xlat, c1, ra, roff);
return CmdRadGrad(index, extend_mode, matrx, xlat, c1, ra, roff);
}
fn read_image(cmd_ix: u32) -> CmdImage {
@ -108,6 +112,25 @@ fn read_end_clip(cmd_ix: u32) -> CmdEndClip {
return CmdEndClip(blend, alpha);
}
fn extend_mode(t: f32, mode: u32) -> f32 {
// This can be replaced with two selects, exchanging the cost
// of a branch for additional ALU
switch mode {
// PAD
case 0u: {
return clamp(t, 0.0, 1.0);
}
// REPEAT
case 1u: {
return fract(t);
}
// REFLECT (2)
default: {
return abs(t - 2.0 * round(0.5 * t));
}
}
}
#else
@group(0) @binding(3)
@ -262,7 +285,7 @@ fn main(
let d = lin.line_x * xy.x + lin.line_y * xy.y + lin.line_c;
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
let my_d = d + lin.line_x * f32(i);
let x = i32(round(clamp(my_d, 0.0, 1.0) * f32(GRADIENT_WIDTH - 1)));
let x = i32(round(extend_mode(my_d, lin.extend_mode) * f32(GRADIENT_WIDTH - 1)));
let fg_rgba = textureLoad(gradients, vec2(x, i32(lin.index)), 0);
let fg_i = fg_rgba * area[i];
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
@ -278,11 +301,16 @@ fn main(
let xy_xformed = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat;
let ba = dot(xy_xformed, rad.c1);
let ca = rad.ra * dot(xy_xformed, xy_xformed);
let t = sqrt(ba * ba + ca) - ba - rad.roff;
let x = i32(round(clamp(t, 0.0, 1.0) * f32(GRADIENT_WIDTH - 1)));
let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0);
let fg_i = fg_rgba * area[i];
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
let t0 = sqrt(ba * ba + ca) - ba;
// For radial gradients that generate a cone, reject pixels outside
// the region.
if t0 >= 0.0 {
let t = t0 - rad.roff;
let x = i32(round(extend_mode(t, rad.extend_mode) * f32(GRADIENT_WIDTH - 1)));
let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0);
let fg_i = fg_rgba * area[i];
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
}
}
cmd_ix += 3u;
}
@ -352,7 +380,7 @@ fn main(
let fg = rgba[i];
// Max with a small epsilon to avoid NaNs
let a_inv = 1.0 / max(fg.a, 1e-6);
let rgba_sep = vec4(fg.rgb * a_inv, fg.a);
let rgba_sep = vec4(fg.rgb * a_inv, fg.a);
textureStore(output, vec2<i32>(coords), rgba_sep);
}
}

View file

@ -306,7 +306,8 @@ fn blend_compose(
let as_fa = as_ * fa;
let ab_fb = ab * fb;
let co = as_fa * cs + ab_fb * cb;
return vec4(co, as_fa + ab_fb);
// Modes like COMPOSE_PLUS can generate alpha > 1.0, so clamp.
return vec4(co, min(as_fa + ab_fb, 1.0));
}
// Apply color mixing and composition. Both input and output colors are

View file

@ -44,6 +44,7 @@ struct CmdColor {
struct CmdLinGrad {
index: u32,
extend_mode: u32,
line_x: f32,
line_y: f32,
line_c: f32,
@ -51,6 +52,7 @@ struct CmdLinGrad {
struct CmdRadGrad {
index: u32,
extend_mode: u32,
matrx: vec4<f32>,
xlat: vec2<f32>,
c1: vec2<f32>,