Skip to content

Commit

Permalink
feat: image border radius rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
mmstick committed Oct 27, 2023
1 parent a9bddc5 commit 8a6f519
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 27 deletions.
13 changes: 11 additions & 2 deletions core/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,18 @@ pub trait Renderer: crate::Renderer {
type Handle: Clone + Hash;

/// Returns the dimensions of an image for the given [`Handle`].
fn dimensions(&self, handle: &Self::Handle) -> Size<u32>;
fn dimensions(
&self,
handle: &Self::Handle,
border_radius: [f32; 4],
) -> Size<u32>;

/// 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],
);
}
6 changes: 5 additions & 1 deletion graphics/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ pub trait Text {
/// A graphics backend that supports image rendering.
pub trait Image {
/// Returns the dimensions of the provided image.
fn dimensions(&self, handle: &image::Handle) -> Size<u32>;
fn dimensions(
&self,
handle: &image::Handle,
border_radius: [f32; 4],
) -> Size<u32>;
}

/// A graphics backend that supports SVG rendering.
Expand Down
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
21 changes: 17 additions & 4 deletions graphics/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,25 @@ where
{
type Handle = image::Handle;

fn dimensions(&self, handle: &image::Handle) -> Size<u32> {
self.backend().dimensions(handle)
fn dimensions(
&self,
handle: &image::Handle,
border_radius: [f32; 4],
) -> Size<u32> {
self.backend().dimensions(handle, border_radius)
}

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
21 changes: 17 additions & 4 deletions renderer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,25 @@ impl<T> text::Renderer for Renderer<T> {
impl<T> crate::core::image::Renderer for Renderer<T> {
type Handle = crate::core::image::Handle;

fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
delegate!(self, renderer, renderer.dimensions(handle))
fn dimensions(
&self,
handle: &crate::core::image::Handle,
border_radius: [f32; 4],
) -> Size<u32> {
delegate!(self, renderer, renderer.dimensions(handle, border_radius))
}

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
24 changes: 19 additions & 5 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 Expand Up @@ -841,8 +851,12 @@ impl backend::Text for Backend {

#[cfg(feature = "image")]
impl backend::Image for Backend {
fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
self.raster_pipeline.dimensions(handle)
fn dimensions(
&self,
handle: &crate::core::image::Handle,
border_radius: [f32; 4],
) -> Size<u32> {
self.raster_pipeline.dimensions(handle, border_radius)
}
}

Expand Down
144 changes: 140 additions & 4 deletions tiny_skia/src/raster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::core::image as raster;
use crate::core::{Rectangle, Size};
use crate::graphics;

use graphics::image::image_rs::{ImageBuffer, Rgba, RgbaImage};
use rustc_hash::{FxHashMap, FxHashSet};
use std::cell::RefCell;
use std::collections::hash_map;
Expand All @@ -17,8 +18,14 @@ impl Pipeline {
}
}

pub fn dimensions(&self, handle: &raster::Handle) -> Size<u32> {
if let Some(image) = self.cache.borrow_mut().allocate(handle) {
pub fn dimensions(
&self,
handle: &raster::Handle,
border_radius: [f32; 4],
) -> Size<u32> {
if let Some(image) =
self.cache.borrow_mut().allocate(handle, border_radius)
{
Size::new(image.width(), image.height())
} else {
Size::new(0, 0)
Expand All @@ -32,8 +39,11 @@ 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(image) =
self.cache.borrow_mut().allocate(handle, border_radius)
{
let width_scale = bounds.width / image.width() as f32;
let height_scale = bounds.height / image.height() as f32;

Expand Down Expand Up @@ -68,11 +78,20 @@ impl Cache {
pub fn allocate(
&mut self,
handle: &raster::Handle,
border_radius: [f32; 4],
) -> Option<tiny_skia::PixmapRef<'_>> {
let id = handle.id();

if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) {
let image = graphics::image::load(handle).ok()?.into_rgba8();
let mut image = graphics::image::load(handle).ok()?.into_rgba8();

// Round the borders if a border radius is defined
if border_radius.iter().any(|&corner| corner != 0.0) {
round(&mut image, {
let [a, b, c, d] = border_radius;
[a as u32, b as u32, c as u32, d as u32]
});
}

let mut buffer =
vec![0u32; image.width() as usize * image.height() as usize];
Expand Down Expand Up @@ -114,3 +133,120 @@ 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 ImageBuffer<Rgba<u8>, Vec<u8>>, radius: [u32; 4]) {
let (width, height) = img.dimensions();
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 ImageBuffer<Rgba<u8>, Vec<u8>>,
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;

let draw = |img: &mut RgbaImage, alpha, x, y| {
debug_assert!((1..=256).contains(&alpha));
let pixel_alpha = &mut img[coordinates(r0 - x, r0 - y)].0[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 {
img[coordinates(r0 - i, r0 - j)].0[3] = 0;
}
}
// remove contents right of current position mirrored
{
let j = x / 16;
for i in y / 16 + 1..r0 {
img[coordinates(r0 - i, r0 - j)].0[3] = 0;
}
}

// 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() {
img[coordinates(r0 - i, r0 - j)].0[3] = 0;
}
}
}
Loading

0 comments on commit 8a6f519

Please sign in to comment.