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:
Orson Peters
2023-05-27 22:55:49 +02:00
committed by Keavon Chambers
parent 0586d52f3a
commit 7148b199ec
15 changed files with 491 additions and 337 deletions

View File

@@ -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;

View 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),
}
}
}

View File

@@ -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)

View 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
}
}

View File

@@ -1,3 +1,4 @@
pub mod brush_stroke;
pub mod consts;
pub mod generator_nodes;
pub mod manipulator_group;

View File

@@ -54,7 +54,7 @@ pub enum TaggedValue {
OptionalColor(Option<graphene_core::raster::color::Color>),
ManipulatorGroupIds(Vec<graphene_core::uuid::ManipulatorGroupId>),
Font(graphene_core::text::Font),
VecDVec2(Vec<DVec2>),
BrushStrokes(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
Segments(Vec<graphene_core::raster::ImageFrame<Color>>),
EditorApi(graphene_core::EditorApi<'static>),
DocumentNode(DocumentNode),
@@ -115,12 +115,7 @@ impl Hash for TaggedValue {
Self::OptionalColor(color) => color.hash(state),
Self::ManipulatorGroupIds(mirror) => mirror.hash(state),
Self::Font(font) => font.hash(state),
Self::VecDVec2(vec_dvec2) => {
vec_dvec2.len().hash(state);
for dvec2 in vec_dvec2 {
dvec2.to_array().iter().for_each(|x| x.to_bits().hash(state));
}
}
Self::BrushStrokes(brush_strokes) => brush_strokes.hash(state),
Self::Segments(segments) => {
for segment in segments {
segment.hash(state)
@@ -176,7 +171,7 @@ impl<'a> TaggedValue {
TaggedValue::OptionalColor(x) => Box::new(x),
TaggedValue::ManipulatorGroupIds(x) => Box::new(x),
TaggedValue::Font(x) => Box::new(x),
TaggedValue::VecDVec2(x) => Box::new(x),
TaggedValue::BrushStrokes(x) => Box::new(x),
TaggedValue::Segments(x) => Box::new(x),
TaggedValue::EditorApi(x) => Box::new(x),
TaggedValue::DocumentNode(x) => Box::new(x),
@@ -239,7 +234,7 @@ impl<'a> TaggedValue {
TaggedValue::OptionalColor(_) => concrete!(Option<graphene_core::Color>),
TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec<graphene_core::uuid::ManipulatorGroupId>),
TaggedValue::Font(_) => concrete!(graphene_core::text::Font),
TaggedValue::VecDVec2(_) => concrete!(Vec<DVec2>),
TaggedValue::BrushStrokes(_) => concrete!(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>),
TaggedValue::EditorApi(_) => concrete!(graphene_core::EditorApi),
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
@@ -291,7 +286,7 @@ impl<'a> TaggedValue {
x if x == TypeId::of::<Option<graphene_core::Color>>() => Some(TaggedValue::OptionalColor(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => Some(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::text::Font>() => Some(TaggedValue::Font(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<DVec2>>() => Some(TaggedValue::VecDVec2(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<graphene_core::vector::brush_stroke::BrushStroke>>() => Some(TaggedValue::BrushStrokes(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>>() => Some(TaggedValue::Segments(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::EditorApi>() => Some(TaggedValue::EditorApi(*downcast(input).unwrap())),
x if x == TypeId::of::<crate::document::DocumentNode>() => Some(TaggedValue::DocumentNode(*downcast(input).unwrap())),

View File

@@ -1,7 +1,7 @@
use std::marker::PhantomData;
use glam::{DAffine2, DVec2};
use graphene_core::raster::{Alpha, Color, Pixel, Sample};
use graphene_core::raster::{Alpha, Color, ImageFrame, Pixel, Sample};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::vector::VectorData;
use graphene_core::Node;
@@ -21,6 +21,22 @@ where
iter.fold(initial, |a, x| lambda.eval((a, x)))
}
#[derive(Clone, Debug, PartialEq)]
pub struct ChainApplyNode<Value> {
pub value: Value,
}
#[node_fn(ChainApplyNode)]
async fn chain_apply<I: Iterator, T>(iter: I, mut value: T) -> T
where
I::Item: for<'a> Node<'a, T, Output = T>,
{
for lambda in iter {
value = lambda.eval(value);
}
value
}
#[derive(Clone, Debug, PartialEq)]
pub struct IntoIterNode<T> {
_t: PhantomData<T>,
@@ -100,7 +116,7 @@ pub struct EraseNode<Flow> {
#[node_fn(EraseNode)]
fn erase(input: (Color, Color), flow: f64) -> Color {
let (input, brush) = input;
let alpha = input.a() * (1.0 - flow as f32 * brush.a());
let alpha = input.a() * (1. - flow as f32 * brush.a());
Color::from_unassociated_alpha(input.r(), input.g(), input.b(), alpha)
}
@@ -134,6 +150,54 @@ fn translate_node<Data: TransformMut>(offset: DVec2, mut translatable: Data) ->
translatable
}
#[derive(Debug, Clone, Copy)]
pub struct BlitNode<P, Texture, Positions, BlendFn> {
texture: Texture,
positions: Positions,
blend_mode: BlendFn,
_p: PhantomData<P>,
}
#[node_fn(BlitNode<_P>)]
fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: ImageFrame<_P>, positions: Vec<DVec2>, blend_mode: BlendFn) -> ImageFrame<_P>
where
BlendFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>,
{
for position in positions {
let target_size = DVec2::new(target.image.width as f64, target.image.height as f64);
let texture_size = DVec2::new(texture.image.width as f64, texture.image.height as f64);
let document_to_target = target.transform.inverse();
let start = document_to_target.transform_point2(position) * target_size - texture_size / 2.;
let stop = start + texture_size;
// Half-open integer ranges [start, stop).
let clamp_start = start.clamp(DVec2::ZERO, target_size).as_uvec2();
let clamp_stop = stop.clamp(DVec2::ZERO, target_size).as_uvec2();
let blit_area_offset = (clamp_start.as_dvec2() - start).as_uvec2().min(texture_size.as_uvec2());
let blit_area_dimensions = (clamp_stop - clamp_start).min(texture_size.as_uvec2() - blit_area_offset);
// Tight blitting loop. Eagerly assert bounds to hopefully eliminate bounds check inside loop.
let texture_index = |x: u32, y: u32| -> usize { (y as usize * texture.image.width as usize) + (x as usize) };
let target_index = |x: u32, y: u32| -> usize { (y as usize * target.image.width as usize) + (x as usize) };
let max_y = (blit_area_offset.y + blit_area_dimensions.y).saturating_sub(1);
let max_x = (blit_area_offset.x + blit_area_dimensions.x).saturating_sub(1);
assert!(texture_index(max_x, max_y) < texture.image.data.len());
assert!(target_index(max_x, max_y) < target.image.data.len());
for y in blit_area_offset.y..blit_area_offset.y + blit_area_dimensions.y {
for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x {
let src_pixel = texture.image.data[texture_index(x, y)];
let dst_pixel = &mut target.image.data[target_index(x + clamp_start.x, y + clamp_start.y)];
*dst_pixel = blend_mode.eval((src_pixel, *dst_pixel));
}
}
}
target
}
#[cfg(test)]
mod test {
use super::*;
@@ -152,10 +216,10 @@ mod test {
fn test_translate_node() {
let image = Image::new(10, 10, Color::TRANSPARENT);
let mut image = ImageFrame { image, transform: DAffine2::IDENTITY };
image.translate(DVec2::new(1.0, 2.0));
image.translate(DVec2::new(1., 2.));
let translate_node = TranslateNode::new(ClonedNode::new(image));
let image = translate_node.eval(DVec2::new(1.0, 2.0));
assert_eq!(image.transform(), DAffine2::from_translation(DVec2::new(2.0, 4.0)));
let image = translate_node.eval(DVec2::new(1., 2.));
assert_eq!(image.transform(), DAffine2::from_translation(DVec2::new(2., 4.)));
}
#[test]
@@ -177,9 +241,9 @@ mod test {
#[test]
fn test_brush() {
let brush_texture_node = BrushStampGeneratorNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(1.0), ClonedNode::new(1.0));
let brush_texture_node = BrushStampGeneratorNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(1.), ClonedNode::new(1.));
let image = brush_texture_node.eval(20.);
let trace = vec![DVec2::new(0.0, 0.0), DVec2::new(10.0, 0.0)];
let trace = vec![DVec2::new(0., 0.), DVec2::new(10., 0.)];
let trace = ClonedNode::new(trace.into_iter());
let translate_node = TranslateNode::new(ClonedNode::new(image));
let frames = MapNode::new(ValueNode::new(translate_node));
@@ -189,7 +253,7 @@ mod test {
let background_bounds = background_bounds.eval(frames.clone().into_iter());
let background_bounds = ClonedNode::new(background_bounds.unwrap().to_transform());
let background_image = background_bounds.then(EmptyImageNode::new(ClonedNode::new(Color::TRANSPARENT)));
let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(1.0));
let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(1.));
let final_image = ReduceNode::new(background_image, ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node))));
let final_image = final_image.eval(frames.into_iter());
assert_eq!(final_image.image.height, 20);

View File

@@ -3,6 +3,7 @@ use glam::{DAffine2, DVec2};
use graphene_core::raster::{Alpha, BlendMode, BlendNode, Image, ImageFrame, Linear, LinearChannel, Luminance, Pixel, RGBMut, Raster, RasterMut, RedGreenBlue, Sample};
use graphene_core::transform::Transform;
use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::value::CopiedNode;
use graphene_core::{Color, Node};
@@ -95,85 +96,6 @@ where
image
}
#[derive(Debug, Clone, DynAny)]
pub struct AxisAlignedBbox {
start: DVec2,
end: DVec2,
}
impl AxisAlignedBbox {
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)]
struct Bbox {
top_left: DVec2,
top_right: DVec2,
bottom_left: DVec2,
bottom_right: DVec2,
}
impl Bbox {
fn 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),
}
}
}
fn compute_transformed_bounding_box(transform: DAffine2) -> Bbox {
let top_left = DVec2::new(0., 1.);
let top_right = DVec2::new(1., 1.);
let bottom_left = DVec2::new(0., 0.);
let bottom_right = DVec2::new(1., 0.);
let transform = |p| transform.transform_point2(p);
Bbox {
top_left: transform(top_left),
top_right: transform(top_right),
bottom_left: transform(bottom_left),
bottom_right: transform(bottom_right),
}
}
#[derive(Debug, Clone, Copy)]
pub struct InsertChannelNode<P, S, Insertion, TargetChannel> {
insertion: Insertion,
@@ -325,8 +247,8 @@ fn blend_new_image<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample<Pixel = _P> +
where
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>,
{
let foreground_aabb = compute_transformed_bounding_box(foreground.transform()).axis_aligned_bbox();
let background_aabb = compute_transformed_bounding_box(background.transform()).axis_aligned_bbox();
let foreground_aabb = Bbox::unit().affine_transform(foreground.transform()).to_axis_aligned_bbox();
let background_aabb = Bbox::unit().affine_transform(background.transform()).to_axis_aligned_bbox();
let Some(aabb) = foreground_aabb.union_non_empty(&background_aabb) else {return ImageFrame::empty()};
@@ -363,7 +285,7 @@ where
let bg_to_fg = background.transform() * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0,0) (1, 1) in the background image space
let bg_aabb = compute_transformed_bounding_box(background.transform().inverse() * foreground.transform()).axis_aligned_bbox();
let bg_aabb = Bbox::unit().affine_transform(background.transform().inverse() * foreground.transform()).to_axis_aligned_bbox();
// Clamp the foreground image to the background image
let start = (bg_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
@@ -393,8 +315,8 @@ pub struct ExtendImageNode<Background> {
#[node_macro::node_fn(ExtendImageNode)]
fn extend_image_node(foreground: ImageFrame<Color>, background: ImageFrame<Color>) -> ImageFrame<Color> {
let foreground_aabb = compute_transformed_bounding_box(foreground.transform()).axis_aligned_bbox();
let background_aabb = compute_transformed_bounding_box(background.transform()).axis_aligned_bbox();
let foreground_aabb = Bbox::unit().affine_transform(foreground.transform()).to_axis_aligned_bbox();
let background_aabb = Bbox::unit().affine_transform(background.transform()).to_axis_aligned_bbox();
if foreground_aabb.contains(background_aabb.start) && foreground_aabb.contains(background_aabb.end) {
return foreground;
@@ -412,7 +334,7 @@ pub struct MergeBoundingBoxNode<Data> {
fn merge_bounding_box_node<_Data: Transform>(input: (Option<AxisAlignedBbox>, _Data)) -> Option<AxisAlignedBbox> {
let (initial_aabb, data) = input;
let snd_aabb = compute_transformed_bounding_box(data.transform()).axis_aligned_bbox();
let snd_aabb = Bbox::unit().affine_transform(data.transform()).to_axis_aligned_bbox();
if let Some(fst_aabb) = initial_aabb {
fst_aabb.union_non_empty(&snd_aabb)

View File

@@ -1,32 +1,27 @@
use glam::{DAffine2, DVec2};
use graph_craft::document::DocumentNode;
use graph_craft::proto::{NodeConstructor, TypeErasedPinned};
use graphene_core::ops::IdNode;
use graphene_core::vector::VectorData;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use graphene_core::quantization::QuantizationChannels;
use graphene_core::raster::bbox::AxisAlignedBbox;
use graphene_core::raster::color::Color;
use graphene_core::structural::Then;
use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
use graphene_core::{fn_type, raster::*};
use graphene_core::{Node, NodeIO, NodeIOTypes};
use graphene_std::brush::*;
use graphene_std::raster::*;
use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyInRefNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode, TypeErasedPinnedRef};
use graphene_core::{Cow, NodeIdentifier, Type, TypeDescriptor};
use graph_craft::proto::{NodeConstructor, TypeErasedPinned};
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::VectorData;
use graphene_core::{concrete, generic, value_fn};
use graphene_core::{fn_type, raster::*};
use graphene_core::{Cow, NodeIdentifier, Type, TypeDescriptor};
use graphene_core::{Node, NodeIO, NodeIOTypes};
use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyInRefNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode, TypeErasedPinnedRef};
use graphene_std::brush::*;
use graphene_std::memo::{CacheNode, LetNode};
use graphene_std::raster::BlendImageTupleNode;
use graphene_std::raster::*;
use dyn_any::StaticType;
use graphene_core::quantization::QuantizationChannels;
use glam::{DAffine2, DVec2};
use once_cell::sync::Lazy;
use std::collections::HashMap;
macro_rules! construct_node {
($args: ident, $path:ty, [$($type:tt),*]) => { async move {
@@ -242,7 +237,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
vec![Type::Fn(Box::new(generic!(T)), Box::new(generic!(V))), Type::Fn(Box::new(generic!(V)), Box::new(generic!(U)))],
),
)],
register_node!(graphene_std::brush::IntoIterNode<_>, input: &Vec<DVec2>, params: []),
register_node!(graphene_std::brush::IntoIterNode<_>, input: &Vec<BrushStroke>, params: []),
vec![(
NodeIdentifier::new("graphene_std::brush::BrushNode"),
|args| {
@@ -253,55 +248,48 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
Box::pin(async move {
let image: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[0]);
let bounds: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[1]);
let trace: DowncastBothNode<(), Vec<DVec2>> = DowncastBothNode::new(args[2]);
let diameter: DowncastBothNode<(), f64> = DowncastBothNode::new(args[3]);
let hardness: DowncastBothNode<(), f64> = DowncastBothNode::new(args[4]);
let flow: DowncastBothNode<(), f64> = DowncastBothNode::new(args[5]);
let color: DowncastBothNode<(), Color> = DowncastBothNode::new(args[6]);
let strokes: DowncastBothNode<(), Vec<BrushStroke>> = DowncastBothNode::new(args[2]);
let stamp = BrushStampGeneratorNode::new(CopiedNode::new(color.eval(()).await), CopiedNode::new(hardness.eval(()).await), CopiedNode::new(flow.eval(()).await));
let stamp = stamp.eval(diameter.eval(()).await);
let frames = TranslateNode::new(CopiedNode::new(stamp));
let frames = MapNode::new(ValueNode::new(frames));
let frames = frames.eval(trace.eval(()).await.into_iter()).collect::<Vec<_>>();
let background_bounds = ReduceNode::new(ClonedNode::new(None), ValueNode::new(MergeBoundingBoxNode::new()));
let background_bounds = background_bounds.eval(frames.clone().into_iter());
let background_bounds = MergeBoundingBoxNode::new().eval((background_bounds, image.eval(()).await));
let mut background_bounds = CopiedNode::new(background_bounds.unwrap().to_transform());
let strokes = strokes.eval(()).await;
let bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
let mut background_bounds = CopiedNode::new(bbox.to_transform());
let bounds_transform = bounds.eval(()).await.transform;
if bounds_transform != DAffine2::ZERO {
background_bounds = CopiedNode::new(bounds_transform);
}
let background_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)));
let blend_node = graphene_core::raster::BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
let blank_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)));
let background = image.and_then(ExtendImageNode::new(blank_image));
let background = ExtendImageNode::new(background_image);
let background_image = image.and_then(background);
let mut blits = Vec::new();
for stroke in strokes {
let stamp = BrushStampGeneratorNode::new(CopiedNode::new(stroke.style.color), CopiedNode::new(stroke.style.hardness), CopiedNode::new(stroke.style.flow));
let stamp = stamp.eval(stroke.style.diameter);
let final_image = ReduceNode::new(ClonedNode::new(background_image.eval(()).await), ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node))));
let final_image = ClonedNode::new(frames.into_iter()).then(final_image);
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(stroke.style.diameter), 0., -DVec2::splat(stroke.style.diameter / 2.0));
let blank_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(transform);
let final_image = FutureWrapperNode::new(final_image);
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(final_image));
any.into_type_erased()
let blend_params = graphene_core::raster::BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params));
let texture = blend_executor.eval((blank_texture, stamp));
let translations: Vec<_> = stroke.compute_blit_points().into_iter().collect();
let blit_node = BlitNode::new(ClonedNode::new(texture), ClonedNode::new(translations), ClonedNode::new(blend_params));
blits.push(blit_node);
}
let all_blits = ChainApplyNode::new(background);
let node = ClonedNode::new(blits.into_iter()).then(all_blits);
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(node));
Box::pin(any) as TypeErasedPinned
})
},
NodeIOTypes::new(
concrete!(()),
concrete!(ImageFrame<Color>),
vec![
value_fn!(ImageFrame<Color>),
value_fn!(ImageFrame<Color>),
value_fn!(Vec<DVec2>),
value_fn!(f64),
value_fn!(f64),
value_fn!(f64),
value_fn!(Color),
],
vec![value_fn!(ImageFrame<Color>), value_fn!(ImageFrame<Color>), value_fn!(Vec<BrushStroke>)],
),
)],
vec![(