diff --git a/crates/encoding/src/encoding.rs b/crates/encoding/src/encoding.rs index 65dcbbb..1fc4a1d 100644 --- a/crates/encoding/src/encoding.rs +++ b/crates/encoding/src/encoding.rs @@ -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, 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, 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, alpha: f32) { + fn add_ramp( + &mut self, + color_stops: impl Iterator, + 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, }); } } diff --git a/crates/encoding/src/resolve.rs b/crates/encoding/src/resolve.rs index 0f72939..4e6dbc6 100644 --- a/crates/encoding/src/resolve.rs +++ b/crates/encoding/src/resolve.rs @@ -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, + /// 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. diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 5e4ca8e..e5aff38 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -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, + 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, diff --git a/shader/fine.wgsl b/shader/fine.wgsl index ed47bcb..42970e5 100644 --- a/shader/fine.wgsl +++ b/shader/fine.wgsl @@ -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(info[info_offset]); let line_y = bitcast(info[info_offset + 1u]); let line_c = bitcast(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(info[info_offset]); let m1 = bitcast(info[info_offset + 1u]); @@ -81,7 +85,7 @@ fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { let c1 = vec2(bitcast(info[info_offset + 6u]), bitcast(info[info_offset + 7u])); let ra = bitcast(info[info_offset + 8u]); let roff = bitcast(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(coords), rgba_sep); } } diff --git a/shader/shared/blend.wgsl b/shader/shared/blend.wgsl index e101c70..e8ec3c7 100644 --- a/shader/shared/blend.wgsl +++ b/shader/shared/blend.wgsl @@ -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 diff --git a/shader/shared/ptcl.wgsl b/shader/shared/ptcl.wgsl index 5d5e528..8137f32 100644 --- a/shader/shared/ptcl.wgsl +++ b/shader/shared/ptcl.wgsl @@ -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, xlat: vec2, c1: vec2,