-
Notifications
You must be signed in to change notification settings - Fork 139
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement remaining rounded corners features and add optimized anti-a…
…liasing
- Loading branch information
Showing
4 changed files
with
371 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,78 +1,318 @@ | ||
use imageflow_types::{Color, RoundCornersMode}; | ||
use crate::graphics::prelude::*; | ||
|
||
fn get_radius(radius: RoundCornersMode, w: u32, h: u32) -> RoundCornersRadius{ | ||
let smallest_dimension = w.min(h) as f32; | ||
match radius{ | ||
RoundCornersMode::Percentage(p) => | ||
RoundCornersRadius::All( | ||
smallest_dimension * | ||
p.min(100f32).max(0f32) / 200f32), | ||
RoundCornersMode::Pixels(p) => | ||
RoundCornersRadius::All(p.max(0f32).min(smallest_dimension / 2f32)), | ||
RoundCornersMode::Circle => | ||
RoundCornersRadius::Circle, | ||
RoundCornersMode::PercentageCustom { top_left, top_right, bottom_right, bottom_left } => | ||
RoundCornersRadius::Custom([ | ||
smallest_dimension * | ||
top_left.min(100f32).max(0f32) / 200f32, | ||
smallest_dimension * | ||
top_right.min(100f32).max(0f32) / 200f32, | ||
smallest_dimension * | ||
bottom_left.min(100f32).max(0f32) / 200f32, | ||
smallest_dimension * | ||
bottom_right.min(100f32).max(0f32) / 200f32 | ||
]), | ||
RoundCornersMode::PixelsCustom { top_left, top_right, bottom_right, bottom_left } => | ||
RoundCornersRadius::Custom([ | ||
top_left.max(0f32).min(smallest_dimension / 2f32), | ||
top_right.max(0f32).min(smallest_dimension / 2f32), | ||
bottom_left.max(0f32).min(smallest_dimension / 2f32), | ||
bottom_right.max(0f32).min(smallest_dimension / 2f32) | ||
]) | ||
} | ||
} | ||
#[derive(Copy, Clone, PartialEq, Debug)] | ||
enum RoundCornersRadius{ | ||
All(f32), | ||
Circle, | ||
Custom([f32;4]) | ||
} | ||
|
||
fn plan_quadrants(radii: RoundCornersRadius, w: u32, h: u32) -> Result<[QuadrantInfo;4], FlowError>{ | ||
// Simplify Circle scenario | ||
if radii == RoundCornersRadius::Circle{ | ||
let smallest_dimension = w.min(h) as f32; | ||
let offset_x = ((w as i64 - (h as i64)).max(0) / 2) as u32; | ||
let offset_y = ((h as i64 - (w as i64)).max(0) / 2) as u32; | ||
let mut quadrants = plan_quadrants( | ||
RoundCornersRadius::All(smallest_dimension / 2f32), w.min(h), w.min(h)) | ||
.map_err(|e| e.at(here!()))?; | ||
for q in quadrants.iter_mut(){ | ||
q.x = q.x + offset_x; | ||
q.y = q.y + offset_y; | ||
q.image_width = w; | ||
q.image_height = h; | ||
q.center_x = q.center_x + offset_x as f32; | ||
q.center_y = q.center_y + offset_y as f32; | ||
} | ||
return Ok(quadrants); | ||
} | ||
// Expand 'all' into corners | ||
if let RoundCornersRadius::All(v) = radii{ | ||
return plan_quadrants(RoundCornersRadius::Custom([v,v,v,v]), w, h).map_err(|e| e.at(here!())); | ||
} | ||
// Ok, deal with radius pixels | ||
if let RoundCornersRadius::Custom([top_left, top_right, bottom_left, bottom_right]) = radii{ | ||
// Integer division so we don't overlap quadrants when dimensions are odd numbers | ||
let right_half_width = w / 2; | ||
let bottom_half_height = h / 2; | ||
let left_half_width = w - right_half_width; | ||
let top_half_height = h - bottom_half_height; | ||
|
||
|
||
fn get_radius_pixels(radius: RoundCornersMode, w: u32, h: u32) -> Result<f32, FlowError>{ | ||
match radius{ | ||
RoundCornersMode::Percentage(p) => Ok(w.min(h) as f32 * p / 200f32), | ||
RoundCornersMode::Pixels(p) => Ok(p), | ||
RoundCornersMode::Circle => Err(unimpl!("RoundCornersMode::Circle is not implemented")), | ||
RoundCornersMode::PercentageCustom {.. } => Err(unimpl!("RoundCornersMode::PercentageCustom is not implemented")), | ||
RoundCornersMode::PixelsCustom {.. } => Err(unimpl!("RoundCornersMode::PixelsCustom is not implemented")) | ||
Ok([QuadrantInfo{ | ||
which: Quadrant::TopLeft, | ||
x: 0, | ||
y: 0, | ||
width: left_half_width, | ||
height: top_half_height, | ||
image_width: w, | ||
image_height: h, | ||
radius: top_left, | ||
center_x: top_left, | ||
center_y: top_left, | ||
is_top: true, | ||
is_left: true, | ||
}, | ||
QuadrantInfo{ | ||
which: Quadrant::TopRight, | ||
x: left_half_width, | ||
y: 0, | ||
width: right_half_width, | ||
height: top_half_height, | ||
image_width: w, | ||
image_height: h, | ||
radius: top_right, | ||
center_x: w as f32 - top_right, | ||
center_y: top_right, | ||
is_top: true, | ||
is_left: false | ||
}, | ||
QuadrantInfo{ | ||
which: Quadrant::BottomLeft, | ||
x: 0, | ||
y: top_half_height, | ||
width: left_half_width, | ||
height: bottom_half_height, | ||
image_width: w, | ||
image_height: h, | ||
radius: bottom_left, | ||
center_x: bottom_left, | ||
center_y: h as f32 - bottom_left, | ||
is_top: false, | ||
is_left: true, | ||
}, | ||
QuadrantInfo{ | ||
which: Quadrant::BottomRight, | ||
x: left_half_width, | ||
y: top_half_height, | ||
width: right_half_width, | ||
height: bottom_half_height, | ||
image_width: w, | ||
image_height: h, | ||
radius: bottom_right, | ||
center_x: w as f32 - bottom_right, | ||
center_y: h as f32 - bottom_right, | ||
is_top: false, | ||
is_left: false, | ||
} | ||
]) | ||
}else { | ||
Err(unimpl!("Enum not handled, must be new")) | ||
} | ||
} | ||
#[derive(Copy, Clone, PartialEq, Debug)] | ||
enum Quadrant{ | ||
TopLeft, | ||
TopRight, | ||
BottomRight, | ||
BottomLeft | ||
} | ||
#[derive(Copy, Clone, PartialEq, Debug)] | ||
struct QuadrantInfo{ | ||
which: Quadrant, | ||
x: u32, | ||
y: u32, | ||
width: u32, | ||
height: u32, | ||
image_width: u32, | ||
image_height: u32, | ||
radius: f32, | ||
center_x: f32, | ||
center_y: f32, | ||
is_top: bool, | ||
is_left: bool, | ||
} | ||
|
||
impl QuadrantInfo{ | ||
fn bottom(&self) -> u32{ | ||
self.y + self.height | ||
} | ||
fn right(&self) -> u32{ | ||
self.x + self.width | ||
} | ||
} | ||
|
||
|
||
// | ||
// fn plan_quadrant(center_x: f32, center_y: f32, | ||
// radius: f32, | ||
// canvas_width: u32, | ||
// canvas_height: u32) -> Result<Vec<QuadrantDrawOp>, FlowError>{ | ||
// | ||
// let mut orders = Vec::with_capacity(radius.ceil() * 8); | ||
// let r2f = radius * radius; | ||
// | ||
// | ||
// | ||
// for y in (0..=radius_ceil).rev(){ | ||
// let yf = y as f32 - 0.5; | ||
// clear_widths.push(radius_ceil - f32::sqrt(r2f - yf * yf).round() as usize); | ||
// } | ||
// } | ||
|
||
pub unsafe fn flow_bitmap_bgra_clear_around_rounded_corners( | ||
b: &mut BitmapWindowMut<u8>, | ||
radius_mode: RoundCornersMode, | ||
color: imageflow_types::Color | ||
round_corners_mode: RoundCornersMode, | ||
color: Color | ||
) -> Result<(), FlowError> { | ||
if b.info().pixel_layout() != PixelLayout::BGRA { | ||
return Err(nerror!(ErrorKind::InvalidArgument)); | ||
return Err(nerror!(ErrorKind::InvalidArgument, "Only BGRA supported for rounded corners")); | ||
} | ||
|
||
let radius = get_radius_pixels(radius_mode, b.w(), b.h())?; | ||
let radius_ceil = radius.ceil() as usize; | ||
let colorcontext = ColorContext::new(WorkingFloatspace::LinearRGB,0f32); | ||
let matte32 = color.to_color_32().map_err(|e| FlowError::from(e).at(here!()))?; | ||
let matte = matte32.to_bgra8(); | ||
|
||
let rf = radius as f32; | ||
let r2f = rf * rf; | ||
let alpha_to_float = (1.0f32) / 255.0f32; | ||
|
||
let mut clear_widths = Vec::with_capacity(radius_ceil); | ||
for y in (0..=radius_ceil).rev(){ | ||
let yf = y as f32 - 0.5; | ||
clear_widths.push(radius_ceil - f32::sqrt(r2f - yf * yf).round() as usize); | ||
} | ||
let matte_a = matte.a as f32 * alpha_to_float; | ||
let matte_b = colorcontext.srgb_to_floatspace(matte.b); | ||
let matte_g = colorcontext.srgb_to_floatspace(matte.g); | ||
let matte_r = colorcontext.srgb_to_floatspace(matte.r); | ||
|
||
let bgcolor = color.to_color_32().unwrap().to_bgra8(); | ||
let w = b.w(); | ||
let h = b.h(); | ||
|
||
let radius_usize = radius_ceil; | ||
let width = b.w() as usize; | ||
let height = b.h() as usize; | ||
//If you created a circle with the surface area of a 1x1 square, this would be its radius | ||
//Useful for calculating pixel intensities while being correct on average regardless of angle | ||
let volumetric_offset = 0.56419f32; | ||
|
||
//eprintln!("color {},{},{},{:?}", bgcolor.r, bgcolor.g, bgcolor.b, bgcolor.a); | ||
let radius_set = get_radius(round_corners_mode, b.w(), b.h()); | ||
let quadrants = plan_quadrants(radius_set,b.w(), b.h()) | ||
.map_err(|e| e.at(here!()))?; | ||
|
||
for y in 0..height{ | ||
if y <= radius_usize || y >= height - radius_usize { | ||
let mut row = b.row_window(y as u32).unwrap(); | ||
for quadrant in quadrants{ | ||
if quadrant.y > 0 && quadrant.which == Quadrant::TopLeft{ | ||
//Clear top rows, must be a circle | ||
b.fill_rectangle(matte32, 0, 0, w, quadrant.y) | ||
.map_err(|e| e.at(here!()))?; | ||
} | ||
if h > quadrant.bottom() && quadrant.which == Quadrant::BottomLeft{ | ||
//Clear bottom rows, must be a circle | ||
b.fill_rectangle(matte32, 0, quadrant.bottom(), w, h) | ||
.map_err(|e| e.at(here!()))?; | ||
} | ||
let radius_ceil = quadrant.radius.ceil() as usize; | ||
let start_y = if quadrant.is_top { quadrant.y as usize } else { quadrant.bottom() as usize - radius_ceil}; | ||
let end_y = if quadrant.is_top { quadrant.y as usize + radius_ceil } else { quadrant.bottom() as usize }; | ||
let start_x = if quadrant.is_left { quadrant.x as usize} else { quadrant.right() as usize - radius_ceil}; | ||
let end_x = if quadrant.is_left { quadrant.x as usize + radius_ceil } else { quadrant.right() as usize }; | ||
|
||
let (clear_x_from, clear_x_to) = if quadrant.is_left { (0, quadrant.x) } else { (quadrant.right(), w)}; | ||
|
||
//Clear the edges for rows where the quadrant isn't rendering an arc | ||
if clear_x_from != clear_x_to{ | ||
for y in (quadrant.y..start_y as u32).chain(end_y as u32..quadrant.bottom()){ | ||
b.fill_rectangle(matte32, clear_x_from, y, clear_x_to, y+1) | ||
.map_err(|e| e.at(here!()))?; | ||
} | ||
} | ||
|
||
// Calculate radii | ||
// Pixels within the radius of solid are never touched | ||
// Pixels within the radius of influence may be aliased | ||
// Pixels outside the radius of influence are replaced with the matte | ||
let radius_of_influence = quadrant.radius + (1f32 - volumetric_offset); | ||
let radius_of_solid = quadrant.radius - volumetric_offset; | ||
let radius_aliasing_width = radius_of_influence - radius_of_solid; | ||
|
||
|
||
let radius_of_influence_squared = radius_of_influence * radius_of_influence; | ||
let radius_of_solid_squared= radius_of_solid * radius_of_solid; | ||
|
||
for y in start_y..end_y{ | ||
let mut row_window = b.row_window(y as u32).unwrap(); | ||
let row_pixels = row_window.slice_of_pixels_first_row().unwrap(); | ||
let yf = y as f32 + 0.5; | ||
let y_dist_from_center = (quadrant.center_y - yf).abs(); | ||
let y_dist_squared = y_dist_from_center * y_dist_from_center; | ||
|
||
let row_width = row.w(); | ||
let slice = row.slice_of_pixels_first_row().unwrap(); | ||
let x_dist_from_center_solid = f32::sqrt((radius_of_solid_squared - y_dist_squared).max(0f32)); | ||
let x_dist_from_center_influenced = f32::sqrt((radius_of_influence_squared - y_dist_squared).max(0f32)); | ||
|
||
let pixels_from_bottom = height - y - 1; | ||
let edge_solid_x1 = (quadrant.center_x - x_dist_from_center_solid).ceil().max(0f32) as usize; | ||
let edge_solid_x2 = (quadrant.center_x + x_dist_from_center_solid).floor().min(w as f32) as usize; | ||
|
||
let nearest_line_index = y.min(pixels_from_bottom); | ||
let edge_influence_x1 = (quadrant.center_x - x_dist_from_center_influenced).floor().max(0f32) as usize; | ||
let edge_influence_x2 = (quadrant.center_x + x_dist_from_center_influenced).ceil().min(w as f32) as usize; | ||
|
||
let mut clear_width = if nearest_line_index < clear_widths.len() { | ||
clear_widths[nearest_line_index] | ||
//Clear what we don't need to alias | ||
if quadrant.is_left { | ||
row_pixels[0..edge_influence_x1].fill(matte.clone()); | ||
} else { | ||
0 | ||
row_pixels[edge_influence_x2..w as usize].fill(matte.clone()); | ||
}; | ||
|
||
//eprintln!("row width {}, slice width {}, bitmap width {}", row_width, slice.len(), width); | ||
if slice.len() != width { panic!("Width mismatch bug"); } | ||
let (alias_from, alias_to) = if quadrant.is_left{ | ||
(edge_influence_x1,edge_solid_x1) | ||
}else{ | ||
(edge_solid_x2, edge_influence_x2) | ||
}; | ||
|
||
clear_width = clear_width.min(width); | ||
for x in alias_from..alias_to{ | ||
let xf = x as f32 + 0.5; | ||
let diff_x = quadrant.center_x - xf; | ||
let distance = (diff_x * diff_x + y_dist_squared).sqrt(); | ||
|
||
if clear_width > 0 { | ||
//eprintln!("clear {}", clear_width); | ||
slice[0..clear_width].fill(bgcolor.clone()); | ||
slice[width-clear_width..width].fill(bgcolor.clone()); | ||
} | ||
if distance > radius_of_influence{ | ||
row_pixels[x] = matte.clone(); | ||
} else if distance > radius_of_solid{ | ||
//Intensity should be 0..1, where 1 is full matte color and 0 is full image color | ||
let intensity = (distance - radius_of_solid) / (radius_aliasing_width); | ||
|
||
} | ||
} | ||
let pixel = row_pixels[x].clone(); | ||
let pixel_a = pixel.a; | ||
let pixel_a_f32 = pixel_a as i32 as f32 * alpha_to_float * (1f32 - intensity); | ||
|
||
let matte_a = (1.0f32 - pixel_a_f32) * matte_a; | ||
let final_a: f32 = matte_a + pixel_a_f32; | ||
row_pixels[x] = rgb::alt::BGRA8 { | ||
b: colorcontext.floatspace_to_srgb( | ||
(colorcontext.srgb_to_floatspace(pixel.b) * pixel_a_f32 + matte_b * matte_a) / final_a), | ||
g: colorcontext.floatspace_to_srgb( | ||
(colorcontext.srgb_to_floatspace(pixel.g) * pixel_a_f32 + matte_g * matte_a) / final_a), | ||
r: colorcontext.floatspace_to_srgb( | ||
(colorcontext.srgb_to_floatspace(pixel.r) * pixel_a_f32 + matte_r * matte_a) / final_a), | ||
a: uchar_clamp_ff(255f32 * final_a) | ||
}; | ||
|
||
} | ||
|
||
} | ||
} | ||
|
||
} | ||
Ok(()) | ||
} |
Oops, something went wrong.
3542b52
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@check-spelling-bot Report
🔴 Please review
Unrecognized words (63)
Previously acknowledged words that are now absent
Bools lfirst llast prefs reimplementation rfirst rlast uft UNSUPPRESSAvailable dictionaries could cover words not in the dictionary
cspell:fullstack/fullstack.txt (181) covers 11 of them
cspell:aws/aws.txt (1485) covers 10 of them
cspell:rust/rust.txt (112) covers 9 of them
cspell:npm/npm.txt (671) covers 8 of them
cspell:java/java.txt (33524) covers 8 of them
cspell:golang/go.txt (7745) covers 6 of them
cspell:scala/scala.txt (2752) covers 4 of them
cspell:lua/lua.txt (391) covers 4 of them
cspell:django/django.txt (2342) covers 4 of them
cspell:ruby/ruby.txt (354) covers 2 of them
cspell:python/python.txt (364) covers 2 of them
Consider adding them using:
To stop checking additional dictionaries, add:
To accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands
... in a clone of the [email protected]:imazen/imageflow.git repository
on the
main
branch:3542b52
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@check-spelling-bot Report
🔴 Please review
Unrecognized words (63)
Previously acknowledged words that are now absent
Bools lfirst llast prefs reimplementation rfirst rlast uft UNSUPPRESSAvailable dictionaries could cover words not in the dictionary
cspell:fullstack/fullstack.txt (181) covers 11 of them
cspell:aws/aws.txt (1485) covers 10 of them
cspell:rust/rust.txt (112) covers 9 of them
cspell:npm/npm.txt (671) covers 8 of them
cspell:java/java.txt (33524) covers 8 of them
cspell:golang/go.txt (7745) covers 6 of them
cspell:scala/scala.txt (2752) covers 4 of them
cspell:lua/lua.txt (391) covers 4 of them
cspell:django/django.txt (2342) covers 4 of them
cspell:ruby/ruby.txt (354) covers 2 of them
cspell:python/python.txt (364) covers 2 of them
Consider adding them using:
To stop checking additional dictionaries, add:
To accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands
... in a clone of the [email protected]:imazen/imageflow.git repository
on the
refs/tags/v1.7.1-rc65
branch: