Skip to content

Commit

Permalink
feat: add border radius to image rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
mmstick authored Nov 3, 2023
1 parent 19765d3 commit 14943ce
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 12 deletions.
7 changes: 6 additions & 1 deletion core/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,10 @@ pub trait Renderer: crate::Renderer {

/// Draws an image with the given [`Handle`] and inside the provided
/// `bounds`.
fn draw(&mut self, handle: Self::Handle, bounds: Rectangle);
fn draw(
&mut self,
handle: Self::Handle,
bounds: Rectangle,
border_radius: [f32; 4],
);
}
2 changes: 2 additions & 0 deletions graphics/src/primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub enum Primitive<T> {
handle: image::Handle,
/// The bounds of the image
bounds: Rectangle,
/// The border radii of the image
border_radius: [f32; 4],
},
/// An SVG primitive
Svg {
Expand Down
13 changes: 11 additions & 2 deletions graphics/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,17 @@ where
self.backend().dimensions(handle)
}

fn draw(&mut self, handle: image::Handle, bounds: Rectangle) {
self.primitives.push(Primitive::Image { handle, bounds })
fn draw(
&mut self,
handle: image::Handle,
bounds: Rectangle,
border_radius: [f32; 4],
) {
self.primitives.push(Primitive::Image {
handle,
bounds,
border_radius,
})
}
}

Expand Down
13 changes: 11 additions & 2 deletions renderer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,17 @@ impl<T> crate::core::image::Renderer for Renderer<T> {
delegate!(self, renderer, renderer.dimensions(handle))
}

fn draw(&mut self, handle: crate::core::image::Handle, bounds: Rectangle) {
delegate!(self, renderer, renderer.draw(handle, bounds));
fn draw(
&mut self,
handle: crate::core::image::Handle,
bounds: Rectangle,
border_radius: [f32; 4],
) {
delegate!(
self,
renderer,
renderer.draw(handle, bounds, border_radius,)
);
}
}

Expand Down
16 changes: 13 additions & 3 deletions tiny_skia/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,11 @@ impl Backend {
);
}
#[cfg(feature = "image")]
Primitive::Image { handle, bounds } => {
Primitive::Image {
handle,
bounds,
border_radius,
} => {
let physical_bounds = (*bounds + translation) * scale_factor;

if !clip_bounds.intersects(&physical_bounds) {
Expand All @@ -418,8 +422,14 @@ impl Backend {
)
.post_scale(scale_factor, scale_factor);

self.raster_pipeline
.draw(handle, *bounds, pixels, transform, clip_mask);
self.raster_pipeline.draw(
handle,
*bounds,
pixels,
transform,
clip_mask,
*border_radius,
);
}
#[cfg(not(feature = "image"))]
Primitive::Image { .. } => {
Expand Down
149 changes: 148 additions & 1 deletion tiny_skia/src/raster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,33 @@ impl Pipeline {
pixels: &mut tiny_skia::PixmapMut<'_>,
transform: tiny_skia::Transform,
clip_mask: Option<&tiny_skia::Mask>,
border_radius: [f32; 4],
) {
if let Some(image) = self.cache.borrow_mut().allocate(handle) {
if let Some(mut image) = self.cache.borrow_mut().allocate(handle) {
let width_scale = bounds.width / image.width() as f32;
let height_scale = bounds.height / image.height() as f32;

let transform = transform.pre_scale(width_scale, height_scale);

let mut scratch;

// Round the borders if a border radius is defined
if border_radius.iter().any(|&corner| corner != 0.0) {
scratch = image.to_owned();
round(&mut scratch.as_mut(), {
let [a, b, c, d] = border_radius;
let scale_by = width_scale.min(height_scale);
let max_radius = image.width().min(image.height()) / 2;
[
((a / scale_by) as u32).max(1).min(max_radius),
((b / scale_by) as u32).max(1).min(max_radius),
((c / scale_by) as u32).max(1).min(max_radius),
((d / scale_by) as u32).max(1).min(max_radius),
]
});
image = scratch.as_ref();
}

pixels.draw_pixmap(
(bounds.x / width_scale) as i32,
(bounds.y / height_scale) as i32,
Expand Down Expand Up @@ -114,3 +134,130 @@ struct Entry {
height: u32,
pixels: Vec<u32>,
}

// https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2
fn round(img: &mut tiny_skia::PixmapMut, radius: [u32; 4]) {
let (width, height) = (img.width(), img.height());
assert!(radius[0] + radius[1] <= width);
assert!(radius[3] + radius[2] <= width);
assert!(radius[0] + radius[3] <= height);
assert!(radius[1] + radius[2] <= height);

// top left
border_radius(img, radius[0], |x, y| (x - 1, y - 1));
// top right
border_radius(img, radius[1], |x, y| (width - x, y - 1));
// bottom right
border_radius(img, radius[2], |x, y| (width - x, height - y));
// bottom left
border_radius(img, radius[3], |x, y| (x - 1, height - y));
}

fn border_radius(
img: &mut tiny_skia::PixmapMut,
r: u32,
coordinates: impl Fn(u32, u32) -> (u32, u32),
) {
if r == 0 {
return;
}
let r0 = r;

// 16x antialiasing: 16x16 grid creates 256 possible shades, great for u8!
let r = 16 * r;

let mut x = 0;
let mut y = r - 1;
let mut p: i32 = 2 - r as i32;

// ...

let mut alpha: u16 = 0;
let mut skip_draw = true;

fn pixel_id(width: u32, (x, y): (u32, u32)) -> usize {
((width as usize * y as usize) + x as usize) * 4
}

let clear_pixel = |img: &mut tiny_skia::PixmapMut, (x, y): (u32, u32)| {
let pixel = pixel_id(img.width(), (x, y));
img.data_mut()[pixel..pixel + 4].copy_from_slice(&[0; 4]);
};

let draw = |img: &mut tiny_skia::PixmapMut, alpha, x, y| {
debug_assert!((1..=256).contains(&alpha));
let pixel = pixel_id(img.width(), coordinates(r0 - x, r0 - y));
let pixel_alpha = &mut img.data_mut()[pixel + 3];
*pixel_alpha = ((alpha * *pixel_alpha as u16 + 128) / 256) as u8;
};

'l: loop {
// (comments for bottom_right case:)
// remove contents below current position
{
let i = x / 16;
for j in y / 16 + 1..r0 {
clear_pixel(img, coordinates(r0 - i, r0 - j));
}
}
// remove contents right of current position mirrored
{
let j = x / 16;
for i in y / 16 + 1..r0 {
clear_pixel(img, coordinates(r0 - i, r0 - j));
}
}

// draw when moving to next pixel in x-direction
if !skip_draw {
draw(img, alpha, x / 16 - 1, y / 16);
draw(img, alpha, y / 16, x / 16 - 1);
alpha = 0;
}

for _ in 0..16 {
skip_draw = false;

if x >= y {
break 'l;
}

alpha += y as u16 % 16 + 1;
if p < 0 {
x += 1;
p += (2 * x + 2) as i32;
} else {
// draw when moving to next pixel in y-direction
if y % 16 == 0 {
draw(img, alpha, x / 16, y / 16);
draw(img, alpha, y / 16, x / 16);
skip_draw = true;
alpha = (x + 1) as u16 % 16 * 16;
}

x += 1;
p -= (2 * (y - x) + 2) as i32;
y -= 1;
}
}
}

// one corner pixel left
if x / 16 == y / 16 {
// column under current position possibly not yet accounted
if x == y {
alpha += y as u16 % 16 + 1;
}
let s = y as u16 % 16 + 1;
let alpha = 2 * alpha - s * s;
draw(img, alpha, x / 16, y / 16);
}

// remove remaining square of content in the corner
let range = y / 16 + 1..r0;
for i in range.clone() {
for j in range.clone() {
clear_pixel(img, coordinates(r0 - i, r0 - j));
}
}
}
22 changes: 19 additions & 3 deletions widget/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use crate::core::{
ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget,
};

use std::borrow::Cow;
use std::hash::Hash;

pub use image::Handle;
Expand Down Expand Up @@ -46,6 +45,7 @@ pub struct Image<'a, Handle> {
width: Length,
height: Length,
content_fit: ContentFit,
border_radius: [f32; 4],
phantom_data: std::marker::PhantomData<&'a ()>,
}

Expand All @@ -64,10 +64,17 @@ impl<'a, Handle> Image<'a, Handle> {
width: Length::Shrink,
height: Length::Shrink,
content_fit: ContentFit::Contain,
border_radius: [0.0; 4],
phantom_data: std::marker::PhantomData,
}
}

/// Sets the border radius of the image.
pub fn border_radius(mut self, border_radius: [f32; 4]) -> Self {
self.border_radius = border_radius;
self
}

/// Sets the width of the [`Image`] boundaries.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
Expand Down Expand Up @@ -134,6 +141,7 @@ pub fn layout<Renderer, Handle>(
width: Length,
height: Length,
content_fit: ContentFit,
border_radius: [f32; 4],
) -> layout::Node
where
Renderer: image::Renderer<Handle = Handle>,
Expand Down Expand Up @@ -172,6 +180,7 @@ pub fn draw<Renderer, Handle>(
layout: Layout<'_>,
handle: &Handle,
content_fit: ContentFit,
border_radius: [f32; 4],
) where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash,
Expand All @@ -194,7 +203,7 @@ pub fn draw<Renderer, Handle>(
..bounds
};

renderer.draw(handle.clone(), drawing_bounds + offset)
renderer.draw(handle.clone(), drawing_bounds + offset, border_radius);
};

if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
Expand Down Expand Up @@ -231,6 +240,7 @@ where
self.width,
self.height,
self.content_fit,
self.border_radius,
)
}

Expand All @@ -244,7 +254,13 @@ where
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(renderer, layout, &self.handle, self.content_fit)
draw(
renderer,
layout,
&self.handle,
self.content_fit,
self.border_radius,
)
}

#[cfg(feature = "a11y")]
Expand Down
1 change: 1 addition & 0 deletions widget/src/image/viewer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ where
y: bounds.y,
..Rectangle::with_size(image_size)
},
[0.0; 4],
)
});
});
Expand Down

0 comments on commit 14943ce

Please sign in to comment.