Add Canvas::set_clip_path — anti-aliased arbitrary-path clipping on both backends
Add `Canvas::set_clip_path(&[PathCmd])`, clipping subsequent draws to an arbitrary vector path with an anti-aliased edge, on both the software and GLES backends. It complements the existing rect clip (`set_clip_rects`) and is what an embedder needs to render a shaped clip — a circular avatar, a rounded card, a `VectorDrawable` mask — rather than a bounding box. Kept general rather than tied to any one consumer. Software backend: rasterise the path into an anti-aliased tiny-skia coverage `Mask` (Winding fill) and install it as the active clip mask. Every software primitive already threads `clip_mask` through tiny-skia (fills, strokes, lines, paths, images, text, blit), so the path clip applies uniformly with smooth edges. `clip_bounds` reports the path's bounding box while it is active. GLES backend: a 1-bit stencil would clip exactly but leave a hard, aliased edge, so instead the clipped draws are captured into an offscreen layer and composited back through an anti-aliased coverage mask. `set_clip_path` rasterises the path coverage (tiny-skia, anti-aliased), uploads it as a mask texture, allocates a full-canvas layer FBO on first use, and redirects subsequent draws to it via `activate_target`. Ending the clip (`clear_clip` / `set_clip_rects` / a new `set_clip_path`) composites the layer back onto the canvas FBO with a new two-sampler program (`CLIP_COMPOSITE_FRAG_SRC`) that multiplies the layer colour by the mask coverage and blends it premultiplied-over. The layer attaches to the canvas's own shadow FBO, so it needs no stencil bits in the EGL config; it is freed and reallocated on resize and freed on drop, and shared programs/uniforms are copied to sub-canvases like the rest. Usage: a path clip is bracketed — `set_clip_path` then, after the clipped draws, `clear_clip` or `set_clip_rects` to flush it (on GLES this is when the layer is composited). Snapshot the prior clip with `clip_bounds` beforehand and restore it with `set_clip_rects` to compose with an outer clip without leaking state. Add an `examples/clip_path.rs` demo (rounded rect, circle, triangle — same smooth result on both backends) and software-backend unit tests covering the bounding box, the empty-path clear, and a pixel-level check that a triangular clip masks a fill to the path silhouette rather than its bounding box. The GLES layer-composite path needs a live GL context and is exercised by the example. Also fix three rustdoc intra-doc-link warnings surfaced along the way: a private-item link in `app.rs` (`scroll`) and the new GLES doc (`SoftwareCanvas::set_clip_path`) demoted to code spans, and a redundant explicit link target in `chassis.rs`.
This commit is contained in:
131
examples/clip_path.rs
Normal file
131
examples/clip_path.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! `cargo run --example clip_path`
|
||||
//!
|
||||
//! Demonstrates [`Canvas::set_clip_path`] — real per-path clipping. Each cell
|
||||
//! fills its whole rect with a background colour, then installs a path clip and
|
||||
//! fills again with a foreground colour: only the pixels inside the path are
|
||||
//! repainted, so the shape's silhouette appears (not its bounding box).
|
||||
//!
|
||||
//! Both backends clip with smooth, anti-aliased edges: the software backend
|
||||
//! with a tiny-skia coverage mask, the GLES backend by compositing an offscreen
|
||||
//! layer through an equivalent coverage mask. The circle and rounded corners
|
||||
//! look the same on both. Esc exits.
|
||||
|
||||
use ltk::{ column, row, text, App, Color, Element, External, Keysym, PathCmd, Rect };
|
||||
|
||||
#[ derive( Clone, Debug ) ]
|
||||
enum Msg {}
|
||||
|
||||
struct Demo;
|
||||
|
||||
const CELL: f32 = 220.0;
|
||||
const MARGIN: f32 = 24.0;
|
||||
|
||||
fn bg() -> Color { Color::rgba( 0.16, 0.18, 0.22, 1.0 ) }
|
||||
fn fg() -> Color { Color::rgba( 0.30, 0.68, 0.90, 1.0 ) }
|
||||
|
||||
/// Rounded rectangle inset by `MARGIN`, corners of `radius`, built from lines
|
||||
/// and quadratic corners.
|
||||
fn rounded_rect_path( r: Rect, radius: f32 ) -> Vec<PathCmd>
|
||||
{
|
||||
let l = r.x + MARGIN;
|
||||
let t = r.y + MARGIN;
|
||||
let right = r.x + r.width - MARGIN;
|
||||
let b = r.y + r.height - MARGIN;
|
||||
vec![
|
||||
PathCmd::MoveTo( l + radius, t ),
|
||||
PathCmd::LineTo( right - radius, t ),
|
||||
PathCmd::QuadTo( right, t, right, t + radius ),
|
||||
PathCmd::LineTo( right, b - radius ),
|
||||
PathCmd::QuadTo( right, b, right - radius, b ),
|
||||
PathCmd::LineTo( l + radius, b ),
|
||||
PathCmd::QuadTo( l, b, l, b - radius ),
|
||||
PathCmd::LineTo( l, t + radius ),
|
||||
PathCmd::QuadTo( l, t, l + radius, t ),
|
||||
PathCmd::Close,
|
||||
]
|
||||
}
|
||||
|
||||
/// Circle centred in the cell, drawn as four cubic-Bézier quadrants.
|
||||
fn circle_path( r: Rect ) -> Vec<PathCmd>
|
||||
{
|
||||
let cx = r.x + r.width / 2.0;
|
||||
let cy = r.y + r.height / 2.0;
|
||||
let rad = ( r.width.min( r.height ) ) / 2.0 - MARGIN;
|
||||
let k = rad * 0.552_284_75; // 4/3 * tan(pi/8): cubic circle approximation
|
||||
vec![
|
||||
PathCmd::MoveTo( cx + rad, cy ),
|
||||
PathCmd::CubicTo( cx + rad, cy + k, cx + k, cy + rad, cx, cy + rad ),
|
||||
PathCmd::CubicTo( cx - k, cy + rad, cx - rad, cy + k, cx - rad, cy ),
|
||||
PathCmd::CubicTo( cx - rad, cy - k, cx - k, cy - rad, cx, cy - rad ),
|
||||
PathCmd::CubicTo( cx + k, cy - rad, cx + rad, cy - k, cx + rad, cy ),
|
||||
PathCmd::Close,
|
||||
]
|
||||
}
|
||||
|
||||
/// Downward triangle inset by `MARGIN` — a polygon clip.
|
||||
fn triangle_path( r: Rect ) -> Vec<PathCmd>
|
||||
{
|
||||
vec![
|
||||
PathCmd::MoveTo( r.x + r.width / 2.0, r.y + MARGIN ),
|
||||
PathCmd::LineTo( r.x + r.width - MARGIN, r.y + r.height - MARGIN ),
|
||||
PathCmd::LineTo( r.x + MARGIN, r.y + r.height - MARGIN ),
|
||||
PathCmd::Close,
|
||||
]
|
||||
}
|
||||
|
||||
/// A cell that paints `bg()` everywhere, then `fg()` clipped to `shape`. The
|
||||
/// previous clip is snapshotted and restored so the clip does not leak to the
|
||||
/// rest of the frame.
|
||||
fn clipped_cell( shape: fn( Rect ) -> Vec<PathCmd> ) -> Element<Msg>
|
||||
{
|
||||
External::cpu( CELL, CELL, move |canvas, rect|
|
||||
{
|
||||
canvas.fill_rect( rect, bg(), 0.0 );
|
||||
let saved = canvas.clip_bounds();
|
||||
canvas.set_clip_path( &shape( rect ) );
|
||||
canvas.fill_rect( rect, fg(), 0.0 );
|
||||
canvas.set_clip_rects( &saved );
|
||||
} ).into()
|
||||
}
|
||||
|
||||
impl App for Demo
|
||||
{
|
||||
type Message = Msg;
|
||||
|
||||
fn view( &self ) -> Element<Msg>
|
||||
{
|
||||
column()
|
||||
.spacing( 16.0 )
|
||||
.padding( 24.0 )
|
||||
.push( text( "Canvas::set_clip_path — rounded rect, circle, triangle" ) )
|
||||
.push(
|
||||
row()
|
||||
.spacing( 16.0 )
|
||||
.push( clipped_cell( |r| rounded_rect_path( r, 32.0 ) ) )
|
||||
.push( clipped_cell( circle_path ) )
|
||||
.push( clipped_cell( triangle_path ) )
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn update( &mut self, _msg: Msg ) {}
|
||||
|
||||
fn on_key( &mut self, keysym: Keysym ) -> Option<Msg>
|
||||
{
|
||||
if keysym == Keysym::Escape
|
||||
{
|
||||
std::process::exit( 0 );
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn background_color( &self ) -> Color
|
||||
{
|
||||
ltk::theme_palette().bg
|
||||
}
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
ltk::run( Demo );
|
||||
}
|
||||
Reference in New Issue
Block a user