mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Make Brush tool use per-stroke options and improve its performance (#1242)
* Laid groundwork for per-stroke brush parameters. * Added new spacing parameter. * Added back interpolation, using spacing parameter. * Move bounding box code into core. * Initial working prototype of per-stroke styles. * Removed now useless brush node properties. * Made default spacing 50% for performance comparison. * Quick and dirty prototype for BlitNode copied from blend. * Fixed error after rebase. * Optimized the blitting loop. * Pretty big optimization for into_flat_u8. * Insert brush node for images * Fix starting position transform * UX polish * Code review nits --------- Co-authored-by: 0hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
0586d52f3a
commit
7148b199ec
@@ -8,6 +8,7 @@ use glam::DVec2;
|
||||
pub use self::color::{Color, Luma};
|
||||
|
||||
pub mod adjustments;
|
||||
pub mod bbox;
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
pub mod brightness_contrast;
|
||||
pub mod color;
|
||||
|
||||
86
node-graph/gcore/src/raster/bbox.rs
Normal file
86
node-graph/gcore/src/raster/bbox.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[derive(Debug, Clone, DynAny)]
|
||||
pub struct AxisAlignedBbox {
|
||||
pub start: DVec2,
|
||||
pub end: DVec2,
|
||||
}
|
||||
|
||||
impl AxisAlignedBbox {
|
||||
pub const ZERO: Self = Self { start: DVec2::ZERO, end: DVec2::ZERO };
|
||||
|
||||
pub fn size(&self) -> DVec2 {
|
||||
self.end - self.start
|
||||
}
|
||||
|
||||
pub fn to_transform(&self) -> DAffine2 {
|
||||
DAffine2::from_translation(self.start) * DAffine2::from_scale(self.size())
|
||||
}
|
||||
|
||||
pub fn contains(&self, point: DVec2) -> bool {
|
||||
point.x >= self.start.x && point.x <= self.end.x && point.y >= self.start.y && point.y <= self.end.y
|
||||
}
|
||||
|
||||
pub fn intersects(&self, other: &AxisAlignedBbox) -> bool {
|
||||
other.start.x <= self.end.x && other.end.x >= self.start.x && other.start.y <= self.end.y && other.end.y >= self.start.y
|
||||
}
|
||||
|
||||
pub fn union(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox {
|
||||
AxisAlignedBbox {
|
||||
start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)),
|
||||
end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)),
|
||||
}
|
||||
}
|
||||
pub fn union_non_empty(&self, other: &AxisAlignedBbox) -> Option<AxisAlignedBbox> {
|
||||
match (self.size() == DVec2::ZERO, other.size() == DVec2::ZERO) {
|
||||
(true, true) => None,
|
||||
(true, _) => Some(other.clone()),
|
||||
(_, true) => Some(self.clone()),
|
||||
_ => Some(AxisAlignedBbox {
|
||||
start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)),
|
||||
end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bbox {
|
||||
pub top_left: DVec2,
|
||||
pub top_right: DVec2,
|
||||
pub bottom_left: DVec2,
|
||||
pub bottom_right: DVec2,
|
||||
}
|
||||
|
||||
impl Bbox {
|
||||
pub fn unit() -> Self {
|
||||
Self {
|
||||
top_left: DVec2::new(0., 1.),
|
||||
top_right: DVec2::new(1., 1.),
|
||||
bottom_left: DVec2::new(0., 0.),
|
||||
bottom_right: DVec2::new(1., 0.),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn affine_transform(self, transform: DAffine2) -> Self {
|
||||
Self {
|
||||
top_left: transform.transform_point2(self.top_left),
|
||||
top_right: transform.transform_point2(self.top_right),
|
||||
bottom_left: transform.transform_point2(self.bottom_left),
|
||||
bottom_right: transform.transform_point2(self.bottom_right),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_axis_aligned_bbox(&self) -> AxisAlignedBbox {
|
||||
let start_x = self.top_left.x.min(self.top_right.x).min(self.bottom_left.x).min(self.bottom_right.x);
|
||||
let start_y = self.top_left.y.min(self.top_right.y).min(self.bottom_left.y).min(self.bottom_right.y);
|
||||
let end_x = self.top_left.x.max(self.top_right.x).max(self.bottom_left.x).max(self.bottom_right.x);
|
||||
let end_y = self.top_left.y.max(self.top_right.y).max(self.bottom_left.y).max(self.bottom_right.y);
|
||||
|
||||
AxisAlignedBbox {
|
||||
start: DVec2::new(start_x, start_y),
|
||||
end: DVec2::new(end_x, end_y),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,25 +143,47 @@ where
|
||||
let Image { width, height, data } = self;
|
||||
assert!(data.len() == width as usize * height as usize);
|
||||
|
||||
let mut result = Vec::with_capacity(data.len() * 4);
|
||||
// Cache the last sRGB value we computed, speeds up fills.
|
||||
let mut last_r = 0.;
|
||||
let mut last_r_srgb = 0u8;
|
||||
let mut last_g = 0.;
|
||||
let mut last_g_srgb = 0u8;
|
||||
let mut last_b = 0.;
|
||||
let mut last_b_srgb = 0u8;
|
||||
|
||||
let mut result = vec![0; data.len() * 4];
|
||||
let mut i = 0;
|
||||
for color in data {
|
||||
let a = color.a().to_f32();
|
||||
if a < 0.5 / 255.0 {
|
||||
// This would map to fully transparent anyway, avoid expensive encoding.
|
||||
result.push(0);
|
||||
result.push(0);
|
||||
result.push(0);
|
||||
result.push(0);
|
||||
} else {
|
||||
let undo_premultiply = 1.0 / a;
|
||||
let r = float_to_srgb_u8(color.r().to_f32() * undo_premultiply);
|
||||
let g = float_to_srgb_u8(color.g().to_f32() * undo_premultiply);
|
||||
let b = float_to_srgb_u8(color.b().to_f32() * undo_premultiply);
|
||||
result.push(r);
|
||||
result.push(g);
|
||||
result.push(b);
|
||||
result.push((a * 255.0 + 0.5) as u8);
|
||||
// Smaller alpha values than this would map to fully transparent
|
||||
// anyway, avoid expensive encoding.
|
||||
if a >= 0.5 / 255. {
|
||||
let undo_premultiply = 1. / a;
|
||||
let r = color.r().to_f32() * undo_premultiply;
|
||||
let g = color.g().to_f32() * undo_premultiply;
|
||||
let b = color.b().to_f32() * undo_premultiply;
|
||||
|
||||
// Compute new sRGB value if necessary.
|
||||
if r != last_r {
|
||||
last_r = r;
|
||||
last_r_srgb = float_to_srgb_u8(r);
|
||||
}
|
||||
if g != last_g {
|
||||
last_g = g;
|
||||
last_g_srgb = float_to_srgb_u8(g);
|
||||
}
|
||||
if b != last_b {
|
||||
last_b = b;
|
||||
last_b_srgb = float_to_srgb_u8(b);
|
||||
}
|
||||
|
||||
result[i] = last_r_srgb;
|
||||
result[i + 1] = last_g_srgb;
|
||||
result[i + 2] = last_b_srgb;
|
||||
result[i + 3] = (a * 255. + 0.5) as u8;
|
||||
}
|
||||
|
||||
i += 4;
|
||||
}
|
||||
|
||||
(result, width, height)
|
||||
|
||||
109
node-graph/gcore/src/vector/brush_stroke.rs
Normal file
109
node-graph/gcore/src/vector/brush_stroke.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::raster::bbox::AxisAlignedBbox;
|
||||
use crate::Color;
|
||||
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
use glam::DVec2;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// The style of a brush.
|
||||
#[derive(Clone, Debug, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct BrushStyle {
|
||||
pub color: Color,
|
||||
pub diameter: f64,
|
||||
pub hardness: f64,
|
||||
pub flow: f64,
|
||||
pub spacing: f64, // Spacing as a fraction of the diameter.
|
||||
}
|
||||
|
||||
impl Default for BrushStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::BLACK,
|
||||
diameter: 40.,
|
||||
hardness: 50.,
|
||||
flow: 100.,
|
||||
spacing: 50., // Percentage of diameter.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for BrushStyle {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.color.hash(state);
|
||||
self.diameter.to_bits().hash(state);
|
||||
self.hardness.to_bits().hash(state);
|
||||
self.flow.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// A single sample of brush parameters across the brush stroke.
|
||||
#[derive(Clone, Debug, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct BrushInputSample {
|
||||
pub position: DVec2,
|
||||
// Future work: pressure, stylus angle, etc.
|
||||
}
|
||||
|
||||
impl Hash for BrushInputSample {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.position.x.to_bits().hash(state);
|
||||
self.position.y.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters for a single stroke brush.
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct BrushStroke {
|
||||
pub style: BrushStyle,
|
||||
pub trace: Vec<BrushInputSample>,
|
||||
}
|
||||
|
||||
impl BrushStroke {
|
||||
pub fn bounding_box(&self) -> AxisAlignedBbox {
|
||||
let radius = self.style.diameter / 2.;
|
||||
self.trace
|
||||
.iter()
|
||||
.map(|sample| AxisAlignedBbox {
|
||||
start: sample.position + DVec2::new(-radius, -radius),
|
||||
end: sample.position + DVec2::new(radius, radius),
|
||||
})
|
||||
.reduce(|a, b| a.union(&b))
|
||||
.unwrap_or(AxisAlignedBbox::ZERO)
|
||||
}
|
||||
|
||||
pub fn compute_blit_points(&self) -> Vec<DVec2> {
|
||||
// We always travel in a straight line towards the next user input,
|
||||
// placing a blit point every time we travelled our spacing distance.
|
||||
let spacing_dist = self.style.spacing / 100. * self.style.diameter;
|
||||
|
||||
let Some(first_sample) = self.trace.first() else { return Vec::new(); };
|
||||
|
||||
let mut cur_pos = first_sample.position;
|
||||
let mut result = vec![cur_pos];
|
||||
let mut dist_until_next_blit = spacing_dist;
|
||||
for sample in &self.trace[1..] {
|
||||
// Travel to the next sample.
|
||||
let delta = sample.position - cur_pos;
|
||||
let mut dist_left = delta.length();
|
||||
let unit_step = delta / dist_left;
|
||||
|
||||
while dist_left >= dist_until_next_blit {
|
||||
// Take a step to the next blit point.
|
||||
cur_pos += dist_until_next_blit * unit_step;
|
||||
dist_left -= dist_until_next_blit;
|
||||
|
||||
// Blit.
|
||||
result.push(cur_pos);
|
||||
dist_until_next_blit = spacing_dist;
|
||||
}
|
||||
|
||||
// Take the partial step to land at the sample.
|
||||
dist_until_next_blit -= dist_left;
|
||||
cur_pos = sample.position;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod brush_stroke;
|
||||
pub mod consts;
|
||||
pub mod generator_nodes;
|
||||
pub mod manipulator_group;
|
||||
|
||||
Reference in New Issue
Block a user